@visns-studio/visns-components 5.12.2 → 5.12.4

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.
@@ -0,0 +1,451 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import Swal from 'sweetalert2';
3
+ import CustomFetch from '../Fetch';
4
+ import styles from '../styles/StagePopupModal.module.scss';
5
+
6
+ const StagePopupModal = ({
7
+ show,
8
+ stage,
9
+ popupConfig,
10
+ onConfirm,
11
+ onCancel,
12
+ entityData = {},
13
+ routeParams = {}
14
+ }) => {
15
+ const [formData, setFormData] = useState({});
16
+ const [fieldOptions, setFieldOptions] = useState({});
17
+ const [loading, setLoading] = useState(false);
18
+ const [validationErrors, setValidationErrors] = useState({});
19
+
20
+ useEffect(() => {
21
+ if (show && popupConfig?.fields) {
22
+ initializeForm();
23
+ }
24
+ }, [show, popupConfig]);
25
+
26
+ // Re-render form fields when fieldOptions change
27
+ useEffect(() => {
28
+ console.log('[StagePopupModal] useEffect triggered:', {
29
+ show,
30
+ loading,
31
+ fieldOptionsKeys: Object.keys(fieldOptions),
32
+ fieldOptions
33
+ });
34
+ if (show && !loading && Object.keys(fieldOptions).length > 0) {
35
+ console.log('[StagePopupModal] Re-rendering form fields after fieldOptions update');
36
+ renderFormFields();
37
+ }
38
+ }, [fieldOptions, loading, formData, validationErrors, show]);
39
+
40
+ const initializeForm = async () => {
41
+ const initialData = {};
42
+ const options = {};
43
+
44
+ setLoading(true);
45
+
46
+ for (const field of popupConfig.fields) {
47
+ // Set default values
48
+ if (field.value !== undefined) {
49
+ initialData[field.id] = field.value;
50
+ } else if (field.urlParam && routeParams[field.urlParam]) {
51
+ initialData[field.id] = routeParams[field.urlParam];
52
+ } else if (entityData[field.id] !== undefined) {
53
+ initialData[field.id] = entityData[field.id];
54
+ }
55
+
56
+ // Load dropdown options
57
+ if (field.type === 'select' && field.url) {
58
+ try {
59
+ let url = field.url;
60
+
61
+ // Replace URL parameters
62
+ Object.keys(routeParams).forEach(key => {
63
+ url = url.replace(`{${key}}`, routeParams[key]);
64
+ });
65
+
66
+ const response = await CustomFetch(url, 'POST');
67
+ console.log(`[StagePopupModal] Response for ${field.id}:`, response);
68
+ console.log(`[StagePopupModal] Response.data:`, response.data);
69
+
70
+ if (response.data && !response.data.error) {
71
+ // Handle both nested (response.data.data) and direct (response.data) array formats
72
+ const optionsData = Array.isArray(response.data.data) ? response.data.data :
73
+ Array.isArray(response.data) ? response.data : [];
74
+ console.log(`[StagePopupModal] Processed options for ${field.id}:`, optionsData);
75
+ options[field.id] = optionsData;
76
+ }
77
+ } catch (error) {
78
+ console.error(`Failed to load options for ${field.id}:`, error);
79
+ options[field.id] = [];
80
+ }
81
+ }
82
+ }
83
+
84
+ console.log(`[StagePopupModal] Final options object:`, options);
85
+ console.log(`[StagePopupModal] Setting fieldOptions state:`, options);
86
+
87
+ setFormData(initialData);
88
+ setFieldOptions(options);
89
+ setLoading(false);
90
+ };
91
+
92
+ const handleFieldChange = (fieldId, value) => {
93
+ setFormData(prev => ({
94
+ ...prev,
95
+ [fieldId]: value
96
+ }));
97
+
98
+ // Clear validation error for this field
99
+ if (validationErrors[fieldId]) {
100
+ setValidationErrors(prev => ({
101
+ ...prev,
102
+ [fieldId]: null
103
+ }));
104
+ }
105
+ };
106
+
107
+ const validateForm = () => {
108
+ const errors = {};
109
+
110
+ popupConfig.fields?.forEach(field => {
111
+ if (field.required && (!formData[field.id] || formData[field.id] === '')) {
112
+ errors[field.id] = `${field.label} is required`;
113
+ }
114
+
115
+ if (field.validation) {
116
+ const value = formData[field.id];
117
+
118
+ if (field.validation.minLength && value && value.length < field.validation.minLength) {
119
+ errors[field.id] = `${field.label} must be at least ${field.validation.minLength} characters`;
120
+ }
121
+
122
+ if (field.validation.maxLength && value && value.length > field.validation.maxLength) {
123
+ errors[field.id] = `${field.label} must not exceed ${field.validation.maxLength} characters`;
124
+ }
125
+ }
126
+ });
127
+
128
+ setValidationErrors(errors);
129
+ return Object.keys(errors).length === 0;
130
+ };
131
+
132
+ const handleConfirm = () => {
133
+ if (!validateForm()) {
134
+ return;
135
+ }
136
+
137
+ const dataToSend = {};
138
+
139
+ // Only include fields specified in saveFields, or all fields if not specified
140
+ const fieldsToSave = popupConfig.saveFields || popupConfig.fields?.map(f => f.id) || [];
141
+
142
+ fieldsToSave.forEach(fieldId => {
143
+ if (formData[fieldId] !== undefined) {
144
+ dataToSend[fieldId] = formData[fieldId];
145
+ }
146
+ });
147
+
148
+ onConfirm(dataToSend);
149
+ };
150
+
151
+ const renderField = (field) => {
152
+ const value = formData[field.id] || '';
153
+ const hasError = validationErrors[field.id];
154
+ const fieldClass = `${styles.field} ${hasError ? styles.fieldError : ''}`;
155
+
156
+ switch (field.type) {
157
+ case 'select':
158
+ return (
159
+ <div key={field.id} className={fieldClass}>
160
+ <label className={styles.label}>
161
+ {field.label}
162
+ {field.required && <span className={styles.required}>*</span>}
163
+ </label>
164
+ <select
165
+ className={styles.select}
166
+ value={value}
167
+ onChange={(e) => handleFieldChange(field.id, e.target.value)}
168
+ disabled={loading}
169
+ >
170
+ <option value="">-- Select {field.label} --</option>
171
+ {(() => {
172
+ console.log(`[StagePopupModal] Rendering JSX options for ${field.id}:`, fieldOptions[field.id]);
173
+ return (fieldOptions[field.id] || []).map(option => (
174
+ <option key={option.id || option.value} value={option.id || option.value}>
175
+ {option.label || option.name || option.text}
176
+ </option>
177
+ ));
178
+ })()}
179
+ </select>
180
+ {hasError && <span className={styles.errorText}>{validationErrors[field.id]}</span>}
181
+ </div>
182
+ );
183
+
184
+ case 'textarea':
185
+ return (
186
+ <div key={field.id} className={fieldClass}>
187
+ <label className={styles.label}>
188
+ {field.label}
189
+ {field.required && <span className={styles.required}>*</span>}
190
+ </label>
191
+ <textarea
192
+ className={styles.textarea}
193
+ value={value}
194
+ onChange={(e) => handleFieldChange(field.id, e.target.value)}
195
+ placeholder={field.placeholder}
196
+ rows={field.rows || 3}
197
+ disabled={loading}
198
+ />
199
+ {hasError && <span className={styles.errorText}>{validationErrors[field.id]}</span>}
200
+ </div>
201
+ );
202
+
203
+ case 'date':
204
+ return (
205
+ <div key={field.id} className={fieldClass}>
206
+ <label className={styles.label}>
207
+ {field.label}
208
+ {field.required && <span className={styles.required}>*</span>}
209
+ </label>
210
+ <input
211
+ type="date"
212
+ className={styles.input}
213
+ value={value}
214
+ onChange={(e) => handleFieldChange(field.id, e.target.value)}
215
+ disabled={loading}
216
+ />
217
+ {hasError && <span className={styles.errorText}>{validationErrors[field.id]}</span>}
218
+ </div>
219
+ );
220
+
221
+ case 'datetime':
222
+ return (
223
+ <div key={field.id} className={fieldClass}>
224
+ <label className={styles.label}>
225
+ {field.label}
226
+ {field.required && <span className={styles.required}>*</span>}
227
+ </label>
228
+ <input
229
+ type="datetime-local"
230
+ className={styles.input}
231
+ value={value}
232
+ onChange={(e) => handleFieldChange(field.id, e.target.value)}
233
+ disabled={loading}
234
+ />
235
+ {hasError && <span className={styles.errorText}>{validationErrors[field.id]}</span>}
236
+ </div>
237
+ );
238
+
239
+ case 'number':
240
+ return (
241
+ <div key={field.id} className={fieldClass}>
242
+ <label className={styles.label}>
243
+ {field.label}
244
+ {field.required && <span className={styles.required}>*</span>}
245
+ </label>
246
+ <input
247
+ type="number"
248
+ className={styles.input}
249
+ value={value}
250
+ onChange={(e) => handleFieldChange(field.id, e.target.value)}
251
+ placeholder={field.placeholder}
252
+ min={field.min}
253
+ max={field.max}
254
+ step={field.step}
255
+ disabled={loading}
256
+ />
257
+ {hasError && <span className={styles.errorText}>{validationErrors[field.id]}</span>}
258
+ </div>
259
+ );
260
+
261
+ case 'text':
262
+ default:
263
+ return (
264
+ <div key={field.id} className={fieldClass}>
265
+ <label className={styles.label}>
266
+ {field.label}
267
+ {field.required && <span className={styles.required}>*</span>}
268
+ </label>
269
+ <input
270
+ type="text"
271
+ className={styles.input}
272
+ value={value}
273
+ onChange={(e) => handleFieldChange(field.id, e.target.value)}
274
+ placeholder={field.placeholder}
275
+ disabled={loading}
276
+ />
277
+ {hasError && <span className={styles.errorText}>{validationErrors[field.id]}</span>}
278
+ </div>
279
+ );
280
+ }
281
+ };
282
+
283
+ useEffect(() => {
284
+ if (show && popupConfig) {
285
+ if (popupConfig.type === 'confirm') {
286
+ // Simple confirmation dialog
287
+ Swal.fire({
288
+ title: popupConfig.title || 'Confirm Action',
289
+ text: popupConfig.message || 'Are you sure you want to proceed?',
290
+ icon: 'question',
291
+ showCancelButton: true,
292
+ confirmButtonText: popupConfig.confirmText || 'Yes, proceed',
293
+ cancelButtonText: popupConfig.cancelText || 'Cancel',
294
+ customClass: {
295
+ popup: styles.swalPopup,
296
+ confirmButton: styles.swalConfirm,
297
+ cancelButton: styles.swalCancel
298
+ }
299
+ }).then((result) => {
300
+ if (result.isConfirmed) {
301
+ onConfirm({});
302
+ } else {
303
+ onCancel();
304
+ }
305
+ });
306
+ } else if (popupConfig.type === 'form') {
307
+ // Custom form dialog
308
+ const formHTML = `
309
+ <div class="${styles.stagePopupForm}">
310
+ ${popupConfig.message ? `<p class="${styles.message}">${popupConfig.message}</p>` : ''}
311
+ <div id="stage-popup-fields" class="${styles.fields}">
312
+ ${loading ? '<div class="' + styles.loading + '">Loading...</div>' : ''}
313
+ </div>
314
+ </div>
315
+ `;
316
+
317
+ Swal.fire({
318
+ title: popupConfig.title || 'Additional Information Required',
319
+ html: formHTML,
320
+ showCancelButton: true,
321
+ confirmButtonText: popupConfig.confirmText || 'Complete Stage',
322
+ cancelButtonText: popupConfig.cancelText || 'Cancel',
323
+ customClass: {
324
+ popup: styles.swalPopup,
325
+ confirmButton: styles.swalConfirm,
326
+ cancelButton: styles.swalCancel,
327
+ htmlContainer: styles.swalHtmlContainer
328
+ },
329
+ didOpen: () => {
330
+ renderFormFields();
331
+ },
332
+ preConfirm: () => {
333
+ if (!validateForm()) {
334
+ Swal.showValidationMessage('Please correct the errors below');
335
+ return false;
336
+ }
337
+ return true;
338
+ }
339
+ }).then((result) => {
340
+ if (result.isConfirmed) {
341
+ handleConfirm();
342
+ } else {
343
+ onCancel();
344
+ }
345
+ });
346
+ }
347
+ }
348
+ }, [show, popupConfig, loading, formData, fieldOptions]);
349
+
350
+ const renderFormFields = () => {
351
+ console.log('[StagePopupModal] renderFormFields called');
352
+ const fieldsContainer = document.getElementById('stage-popup-fields');
353
+ console.log('[StagePopupModal] fieldsContainer:', fieldsContainer);
354
+ console.log('[StagePopupModal] loading:', loading);
355
+
356
+ if (!fieldsContainer || loading) {
357
+ console.log('[StagePopupModal] Early return - no container or loading');
358
+ return;
359
+ }
360
+
361
+ console.log('[StagePopupModal] Clearing container and rendering fields');
362
+ fieldsContainer.innerHTML = '';
363
+
364
+ console.log('[StagePopupModal] popupConfig.fields:', popupConfig.fields);
365
+ popupConfig.fields?.forEach(field => {
366
+ console.log(`[StagePopupModal] Processing field: ${field.id}, type: ${field.type}`);
367
+ const fieldElement = document.createElement('div');
368
+ fieldElement.className = styles.fieldWrapper;
369
+
370
+ const fieldHTML = renderFieldHTML(field);
371
+ console.log(`[StagePopupModal] Generated HTML for ${field.id}:`, fieldHTML);
372
+ fieldElement.innerHTML = fieldHTML;
373
+
374
+ fieldsContainer.appendChild(fieldElement);
375
+
376
+ // Add event listeners
377
+ const input = fieldElement.querySelector('input, select, textarea');
378
+ if (input) {
379
+ input.addEventListener('change', (e) => {
380
+ handleFieldChange(field.id, e.target.value);
381
+ });
382
+ }
383
+ });
384
+ };
385
+
386
+ const renderFieldHTML = (field) => {
387
+ const value = formData[field.id] || '';
388
+ const hasError = validationErrors[field.id];
389
+ const requiredStar = field.required ? '<span class="' + styles.required + '">*</span>' : '';
390
+
391
+ switch (field.type) {
392
+ case 'select':
393
+ console.log(`[StagePopupModal] Rendering HTML options for ${field.id}:`, fieldOptions[field.id]);
394
+ const options = (fieldOptions[field.id] || []).map(option =>
395
+ `<option value="${option.id || option.value}" ${value === (option.id || option.value) ? 'selected' : ''}>
396
+ ${option.label || option.name || option.text}
397
+ </option>`
398
+ ).join('');
399
+
400
+ return `
401
+ <label class="${styles.label}">${field.label}${requiredStar}</label>
402
+ <select class="${styles.select}" data-field-id="${field.id}">
403
+ <option value="">-- Select ${field.label} --</option>
404
+ ${options}
405
+ </select>
406
+ ${hasError ? `<span class="${styles.errorText}">${validationErrors[field.id]}</span>` : ''}
407
+ `;
408
+
409
+ case 'textarea':
410
+ return `
411
+ <label class="${styles.label}">${field.label}${requiredStar}</label>
412
+ <textarea class="${styles.textarea}" data-field-id="${field.id}" placeholder="${field.placeholder || ''}" rows="${field.rows || 3}">${value}</textarea>
413
+ ${hasError ? `<span class="${styles.errorText}">${validationErrors[field.id]}</span>` : ''}
414
+ `;
415
+
416
+ case 'date':
417
+ return `
418
+ <label class="${styles.label}">${field.label}${requiredStar}</label>
419
+ <input type="date" class="${styles.input}" data-field-id="${field.id}" value="${value}">
420
+ ${hasError ? `<span class="${styles.errorText}">${validationErrors[field.id]}</span>` : ''}
421
+ `;
422
+
423
+ case 'datetime':
424
+ return `
425
+ <label class="${styles.label}">${field.label}${requiredStar}</label>
426
+ <input type="datetime-local" class="${styles.input}" data-field-id="${field.id}" value="${value}">
427
+ ${hasError ? `<span class="${styles.errorText}">${validationErrors[field.id]}</span>` : ''}
428
+ `;
429
+
430
+ case 'number':
431
+ return `
432
+ <label class="${styles.label}">${field.label}${requiredStar}</label>
433
+ <input type="number" class="${styles.input}" data-field-id="${field.id}" value="${value}" placeholder="${field.placeholder || ''}" min="${field.min || ''}" max="${field.max || ''}" step="${field.step || ''}">
434
+ ${hasError ? `<span class="${styles.errorText}">${validationErrors[field.id]}</span>` : ''}
435
+ `;
436
+
437
+ case 'text':
438
+ default:
439
+ return `
440
+ <label class="${styles.label}">${field.label}${requiredStar}</label>
441
+ <input type="text" class="${styles.input}" data-field-id="${field.id}" value="${value}" placeholder="${field.placeholder || ''}">
442
+ ${hasError ? `<span class="${styles.errorText}">${validationErrors[field.id]}</span>` : ''}
443
+ `;
444
+ }
445
+ };
446
+
447
+ // This component doesn't render directly - it uses SweetAlert2 for the modal
448
+ return null;
449
+ };
450
+
451
+ export default StagePopupModal;
@@ -0,0 +1,234 @@
1
+ .stagePopupForm {
2
+ text-align: left;
3
+ min-width: 400px;
4
+ }
5
+
6
+ .message {
7
+ margin-bottom: 20px;
8
+ color: #666;
9
+ font-size: 14px;
10
+ line-height: 1.4;
11
+ }
12
+
13
+ .fields {
14
+ display: flex;
15
+ flex-direction: column;
16
+ gap: 16px;
17
+ }
18
+
19
+ .fieldWrapper {
20
+ display: flex;
21
+ flex-direction: column;
22
+ gap: 6px;
23
+ }
24
+
25
+ .field {
26
+ display: flex;
27
+ flex-direction: column;
28
+ gap: 6px;
29
+ }
30
+
31
+ .fieldError {
32
+ .input,
33
+ .select,
34
+ .textarea {
35
+ border-color: #e74c3c;
36
+ box-shadow: 0 0 0 2px rgba(231, 76, 60, 0.1);
37
+ }
38
+ }
39
+
40
+ .label {
41
+ font-weight: 500;
42
+ color: #333;
43
+ font-size: 14px;
44
+ margin-bottom: 4px;
45
+ }
46
+
47
+ .required {
48
+ color: #e74c3c;
49
+ margin-left: 2px;
50
+ }
51
+
52
+ .input,
53
+ .select,
54
+ .textarea {
55
+ padding: 8px 12px;
56
+ border: 1px solid #ddd;
57
+ border-radius: 4px;
58
+ font-size: 14px;
59
+ font-family: inherit;
60
+ transition: border-color 0.2s ease, box-shadow 0.2s ease;
61
+
62
+ &:focus {
63
+ outline: none;
64
+ border-color: #3498db;
65
+ box-shadow: 0 0 0 2px rgba(52, 152, 219, 0.1);
66
+ }
67
+
68
+ &:disabled {
69
+ background-color: #f8f9fa;
70
+ color: #6c757d;
71
+ cursor: not-allowed;
72
+ }
73
+ }
74
+
75
+ .select {
76
+ cursor: pointer;
77
+
78
+ &:disabled {
79
+ cursor: not-allowed;
80
+ }
81
+ }
82
+
83
+ .textarea {
84
+ resize: vertical;
85
+ min-height: 80px;
86
+ }
87
+
88
+ .errorText {
89
+ color: #e74c3c;
90
+ font-size: 12px;
91
+ margin-top: 4px;
92
+ }
93
+
94
+ .loading {
95
+ text-align: center;
96
+ padding: 20px;
97
+ color: #666;
98
+ font-style: italic;
99
+ }
100
+
101
+ // SweetAlert2 customizations
102
+ .swalPopup {
103
+ border-radius: 8px;
104
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
105
+ }
106
+
107
+ .swalHtmlContainer {
108
+ padding: 0 !important;
109
+ margin: 16px 0 !important;
110
+ }
111
+
112
+ .swalConfirm {
113
+ background-color: #3498db !important;
114
+ border: none !important;
115
+ border-radius: 4px !important;
116
+ font-weight: 500 !important;
117
+ padding: 10px 20px !important;
118
+
119
+ &:hover {
120
+ background-color: #2980b9 !important;
121
+ }
122
+
123
+ &:focus {
124
+ box-shadow: 0 0 0 2px rgba(52, 152, 219, 0.3) !important;
125
+ }
126
+ }
127
+
128
+ .swalCancel {
129
+ background-color: #95a5a6 !important;
130
+ border: none !important;
131
+ border-radius: 4px !important;
132
+ font-weight: 500 !important;
133
+ padding: 10px 20px !important;
134
+
135
+ &:hover {
136
+ background-color: #7f8c8d !important;
137
+ }
138
+
139
+ &:focus {
140
+ box-shadow: 0 0 0 2px rgba(149, 165, 166, 0.3) !important;
141
+ }
142
+ }
143
+
144
+ // Responsive adjustments
145
+ @media (max-width: 768px) {
146
+ .stagePopupForm {
147
+ min-width: 300px;
148
+ }
149
+
150
+ .fields {
151
+ gap: 12px;
152
+ }
153
+
154
+ .input,
155
+ .select,
156
+ .textarea {
157
+ font-size: 16px; // Prevents zoom on iOS
158
+ }
159
+ }
160
+
161
+ // Theme variations
162
+ .stagePopupForm {
163
+ &.theme-dark {
164
+ .label {
165
+ color: #f8f9fa;
166
+ }
167
+
168
+ .message {
169
+ color: #adb5bd;
170
+ }
171
+
172
+ .input,
173
+ .select,
174
+ .textarea {
175
+ background-color: #343a40;
176
+ border-color: #495057;
177
+ color: #f8f9fa;
178
+
179
+ &:focus {
180
+ border-color: #3498db;
181
+ box-shadow: 0 0 0 2px rgba(52, 152, 219, 0.2);
182
+ }
183
+
184
+ &:disabled {
185
+ background-color: #495057;
186
+ color: #6c757d;
187
+ }
188
+ }
189
+
190
+ .select option {
191
+ background-color: #343a40;
192
+ color: #f8f9fa;
193
+ }
194
+ }
195
+ }
196
+
197
+ // Animation for field validation
198
+ .field {
199
+ transition: all 0.2s ease;
200
+ }
201
+
202
+ .fieldError {
203
+ animation: shake 0.3s ease-in-out;
204
+ }
205
+
206
+ @keyframes shake {
207
+ 0%, 100% { transform: translateX(0); }
208
+ 25% { transform: translateX(-2px); }
209
+ 75% { transform: translateX(2px); }
210
+ }
211
+
212
+ // Loading state
213
+ .loading {
214
+ position: relative;
215
+
216
+ &::after {
217
+ content: '';
218
+ position: absolute;
219
+ top: 50%;
220
+ right: 20px;
221
+ transform: translateY(-50%);
222
+ width: 16px;
223
+ height: 16px;
224
+ border: 2px solid #ddd;
225
+ border-top: 2px solid #3498db;
226
+ border-radius: 50%;
227
+ animation: spin 1s linear infinite;
228
+ }
229
+ }
230
+
231
+ @keyframes spin {
232
+ 0% { transform: translateY(-50%) rotate(0deg); }
233
+ 100% { transform: translateY(-50%) rotate(360deg); }
234
+ }
@@ -1,5 +1,5 @@
1
1
  import Swal from 'sweetalert2';
2
- import '../crm/generic/styles/SweetAlert.module.css';
2
+ import '../styles/SweetAlert.module.css';
3
3
 
4
4
  /**
5
5
  * A utility function that replaces react-confirm-alert with SweetAlert2