@visns-studio/visns-components 5.12.3 → 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.
package/package.json CHANGED
@@ -87,7 +87,7 @@
87
87
  "react-dom": "^17.0.0 || ^18.0.0"
88
88
  },
89
89
  "name": "@visns-studio/visns-components",
90
- "version": "5.12.3",
90
+ "version": "5.12.4",
91
91
  "description": "Various packages to assist in the development of our Custom Applications.",
92
92
  "main": "src/index.js",
93
93
  "files": [
@@ -40,9 +40,12 @@ import DateFilter from '@visns-studio/visns-datagrid-enterprise/DateFilter';
40
40
  import ReactDataGrid from '@visns-studio/visns-datagrid-enterprise';
41
41
  import { toast } from 'react-toastify';
42
42
  import fetchUtil from '../utils/fetchUtil';
43
- import { confirmDialog } from '../utils/ConfirmDialog';
43
+ import { confirmDialog } from './utils/ConfirmDialog';
44
44
  import { DEFAULT_INTELLIGENT_SORTING_CONFIG } from '../utils/relationshipSortingUtils';
45
- import { extractColumnsMetadata, isColumnSortable } from '../utils/columnsMetadataUtils';
45
+ import {
46
+ extractColumnsMetadata,
47
+ isColumnSortable,
48
+ } from '../utils/columnsMetadataUtils';
46
49
  import {
47
50
  AlarmClock,
48
51
  RotateCcw,
@@ -207,10 +210,10 @@ const loadData = async (
207
210
  try {
208
211
  const response = await fetchUtil.post(url, params);
209
212
  const { data, total } = response.data;
210
-
213
+
211
214
  // Extract columns metadata if available
212
215
  const metadata = extractColumnsMetadata(response);
213
-
216
+
214
217
  return { data, count: total, metadata };
215
218
  } catch (error) {
216
219
  console.error('Error fetching data:', error);
@@ -391,7 +394,7 @@ const DataGrid = forwardRef(
391
394
  if (res.metadata) {
392
395
  setColumnsMetadata(res.metadata);
393
396
  }
394
-
397
+
395
398
  // Sort data by grouping field if grouping is enabled
396
399
  if (ajaxSetting?.groupBy?.length > 0) {
397
400
  const groupField = ajaxSetting.groupBy[0]; // groupBy is array of strings
@@ -449,9 +452,9 @@ const DataGrid = forwardRef(
449
452
  }, 10);
450
453
  });
451
454
 
452
- return sqlResult.then(res => ({
455
+ return sqlResult.then((res) => ({
453
456
  data: res.data,
454
- count: res.count
457
+ count: res.count,
455
458
  }));
456
459
  },
457
460
  [ajaxSetting, dSearch, collapsedGroups]
@@ -872,15 +875,12 @@ const DataGrid = forwardRef(
872
875
  case 'file':
873
876
  case 'image':
874
877
  case 'undo':
878
+ case 'restore':
875
879
  // Execute the action directly since confirmation was already handled
876
- CustomFetch(
877
- s.url,
878
- 'POST',
879
- { id: d[s.key] },
880
- (result) => {
881
- toast.success(result.message);
882
- }
883
- );
880
+ CustomFetch(s.url, 'POST', { id: d[s.key] }, (result) => {
881
+ toast.success(result.message);
882
+ handleReload(); // Add reload to refresh data after action
883
+ });
884
884
  break;
885
885
  case 'clone':
886
886
  if (s.url && s.url !== '') {
@@ -1346,17 +1346,19 @@ const DataGrid = forwardRef(
1346
1346
  if (iconConfig.formModal) {
1347
1347
  // Get all rows in the group for bulk editing
1348
1348
  const groupRows = getGroupRows(groupValue);
1349
- const groupRowIds = groupRows.map(row => row[form?.primaryKey || 'id']);
1350
-
1349
+ const groupRowIds = groupRows.map(
1350
+ (row) => row[form?.primaryKey || 'id']
1351
+ );
1352
+
1351
1353
  // Set bulk edit data
1352
1354
  setBulkEditMode(true);
1353
1355
  setBulkEditGroupData({
1354
1356
  groupValue: groupValue,
1355
1357
  groupRows: groupRows,
1356
1358
  groupRowIds: groupRowIds,
1357
- iconConfig: iconConfig
1359
+ iconConfig: iconConfig,
1358
1360
  });
1359
-
1361
+
1360
1362
  // Open modal for bulk editing
1361
1363
  modalOpen('update', null);
1362
1364
  } else {
@@ -2374,8 +2376,11 @@ const DataGrid = forwardRef(
2374
2376
 
2375
2377
  // Check if column is sortable based on metadata
2376
2378
  const columnKey = column.id;
2377
- const sortable = isColumnSortable(columnKey, columnsMetadata);
2378
-
2379
+ const sortable = isColumnSortable(
2380
+ columnKey,
2381
+ columnsMetadata
2382
+ );
2383
+
2379
2384
  const commonProps = {
2380
2385
  header: column.label,
2381
2386
  defaultFlex: 1,
@@ -2481,20 +2486,26 @@ const DataGrid = forwardRef(
2481
2486
  ? column.filter.placeholder
2482
2487
  : 'All Options',
2483
2488
  dataSource: selectedDataSource,
2484
- searchable: column.filter.searchable !== false,
2489
+ searchable:
2490
+ column.filter.searchable !== false,
2485
2491
  // Enable clear functionality even when searchable is false
2486
- allowClearWhenNotSearchable: column.filter.searchable === false,
2492
+ allowClearWhenNotSearchable:
2493
+ column.filter.searchable === false,
2487
2494
  // Explicitly enable clear icon
2488
2495
  clearIcon: true,
2489
2496
  };
2490
-
2497
+
2491
2498
  // Debug: Check if fix is applied
2492
2499
  if (column.filter.searchable === false) {
2493
- console.log('Filter Debug - searchable=false:', {
2494
- columnName: column.name || 'unknown',
2495
- props: filterEditorProps,
2496
- visnsDataGridVersion: 'v1.0.18'
2497
- });
2500
+ console.log(
2501
+ 'Filter Debug - searchable=false:',
2502
+ {
2503
+ columnName:
2504
+ column.name || 'unknown',
2505
+ props: filterEditorProps,
2506
+ visnsDataGridVersion: 'v1.0.18',
2507
+ }
2508
+ );
2498
2509
  }
2499
2510
  break;
2500
2511
  default:
@@ -2638,7 +2649,9 @@ const DataGrid = forwardRef(
2638
2649
  filterEditor,
2639
2650
  filterEditorProps,
2640
2651
  relationName,
2641
- intelligentSortingConfig: tableSetting?.intelligentSorting || DEFAULT_INTELLIGENT_SORTING_CONFIG,
2652
+ intelligentSortingConfig:
2653
+ tableSetting?.intelligentSorting ||
2654
+ DEFAULT_INTELLIGENT_SORTING_CONFIG,
2642
2655
  });
2643
2656
  case 'relationArray':
2644
2657
  return renderRelationArrayColumn({
@@ -2647,7 +2660,9 @@ const DataGrid = forwardRef(
2647
2660
  filterEditor,
2648
2661
  filterEditorProps,
2649
2662
  relationName,
2650
- intelligentSortingConfig: tableSetting?.intelligentSorting || DEFAULT_INTELLIGENT_SORTING_CONFIG,
2663
+ intelligentSortingConfig:
2664
+ tableSetting?.intelligentSorting ||
2665
+ DEFAULT_INTELLIGENT_SORTING_CONFIG,
2651
2666
  });
2652
2667
  case 'richtext':
2653
2668
  return renderRichTextColumn({
@@ -2697,22 +2712,37 @@ const DataGrid = forwardRef(
2697
2712
  : column.id;
2698
2713
 
2699
2714
  // Auto-detect columns that should have word wrap enabled
2700
- const shouldAutoEnableWordWrap =
2715
+ const shouldAutoEnableWordWrap =
2701
2716
  column.wordWrap !== false && // Allow explicit disable with wordWrap: false
2702
- (
2703
- // Text-heavy column patterns
2704
- /^(task|description|notes?|comments?|content|message|details?|summary|reason|remarks?)$/i.test(columnId) ||
2705
- /.*_(task|description|notes?|comments?|content|message|details?|summary|reason|remarks?)$/i.test(columnId) ||
2706
- /(task|description|notes?|comments?|content|message|details?|summary|reason|remarks?)_.*$/i.test(columnId) ||
2717
+ // Text-heavy column patterns
2718
+ (/^(task|description|notes?|comments?|content|message|details?|summary|reason|remarks?)$/i.test(
2719
+ columnId
2720
+ ) ||
2721
+ /.*_(task|description|notes?|comments?|content|message|details?|summary|reason|remarks?)$/i.test(
2722
+ columnId
2723
+ ) ||
2724
+ /(task|description|notes?|comments?|content|message|details?|summary|reason|remarks?)_.*$/i.test(
2725
+ columnId
2726
+ ) ||
2707
2727
  // Check for long labels that might indicate text content
2708
- (column.label && column.label.length > 15 && /description|notes|comments|details|content/i.test(column.label))
2709
- );
2728
+ (column.label &&
2729
+ column.label.length > 15 &&
2730
+ /description|notes|comments|details|content/i.test(
2731
+ column.label
2732
+ )));
2710
2733
 
2711
- const finalWordWrap = column.wordWrap === true || shouldAutoEnableWordWrap;
2734
+ const finalWordWrap =
2735
+ column.wordWrap === true ||
2736
+ shouldAutoEnableWordWrap;
2712
2737
 
2713
2738
  // Debug logging for word wrap detection
2714
- if (shouldAutoEnableWordWrap && column.wordWrap !== true) {
2715
- console.log(`DataGrid: Auto-enabled wordWrap for column "${columnId}" (${column.label})`);
2739
+ if (
2740
+ shouldAutoEnableWordWrap &&
2741
+ column.wordWrap !== true
2742
+ ) {
2743
+ console.log(
2744
+ `DataGrid: Auto-enabled wordWrap for column "${columnId}" (${column.label})`
2745
+ );
2716
2746
  }
2717
2747
 
2718
2748
  return {
@@ -2720,7 +2750,9 @@ const DataGrid = forwardRef(
2720
2750
  name: columnId,
2721
2751
  filterEditor: filterEditor,
2722
2752
  filterEditorProps: filterEditorProps,
2723
- className: finalWordWrap ? 'cell-word-wrap' : '',
2753
+ className: finalWordWrap
2754
+ ? 'cell-word-wrap'
2755
+ : '',
2724
2756
  render: ({ data }) => {
2725
2757
  if (
2726
2758
  data &&
@@ -2751,7 +2783,10 @@ const DataGrid = forwardRef(
2751
2783
  }
2752
2784
 
2753
2785
  // If removeFormat or stripHtml is true, strip HTML tags
2754
- if (column.removeFormat || column.stripHtml) {
2786
+ if (
2787
+ column.removeFormat ||
2788
+ column.stripHtml
2789
+ ) {
2755
2790
  content = content.replace(
2756
2791
  /<\/?[^>]+(>|$)/g,
2757
2792
  ''
@@ -3093,15 +3128,22 @@ const DataGrid = forwardRef(
3093
3128
  {/* Top container with search, action buttons, and auto-refresh */}
3094
3129
  {/* Only show top container if there are controls to display */}
3095
3130
  {(() => {
3096
- const hasSearchOrCreate = !form.searchDisable || !form.createDisable;
3097
- const hasAutoRefresh = !shouldRelocateAutoRefresh() && ajaxSetting && ajaxSetting.autoRefresh !== undefined;
3098
-
3099
- // If DataGridSearch would return null (both search and create disabled),
3131
+ const hasSearchOrCreate =
3132
+ !form.searchDisable || !form.createDisable;
3133
+ const hasAutoRefresh =
3134
+ !shouldRelocateAutoRefresh() &&
3135
+ ajaxSetting &&
3136
+ ajaxSetting.autoRefresh !== undefined;
3137
+
3138
+ // If DataGridSearch would return null (both search and create disabled),
3100
3139
  // only show container if there are other controls besides auto-refresh
3101
- if (form.searchDisable === true && form.createDisable === true) {
3140
+ if (
3141
+ form.searchDisable === true &&
3142
+ form.createDisable === true
3143
+ ) {
3102
3144
  return false; // Hide AutoRefreshControls when it would be the only control
3103
3145
  }
3104
-
3146
+
3105
3147
  return hasSearchOrCreate || hasAutoRefresh;
3106
3148
  })() && (
3107
3149
  <div className={styles.dataGridTopContainer}>
@@ -71,6 +71,7 @@ import GenericEditableTable from './GenericEditableTable';
71
71
  import MergeEntity from '../MergeEntity';
72
72
  import MultiSelect from '../MultiSelect';
73
73
  import QrCode from '../QrCode';
74
+ import StagePopupModal from './StagePopupModal';
74
75
  import Table from '../DataGrid';
75
76
  import TableFilter from '../TableFilter';
76
77
 
@@ -157,6 +158,10 @@ function GenericDetail({
157
158
  const [total, setTotal] = useState(0);
158
159
  const [windowHeight, setWindowHeight] = useState(window.innerHeight);
159
160
 
161
+ /** Stage Popup States */
162
+ const [showStagePopup, setShowStagePopup] = useState(false);
163
+ const [stagePopupData, setStagePopupData] = useState(null);
164
+
160
165
  /** Ocr States */
161
166
  const [ocrRowsSelected, setOcrRowsSelected] = useState('');
162
167
 
@@ -297,6 +302,45 @@ function GenericDetail({
297
302
  };
298
303
 
299
304
  const handleStageUpdate = async (stage, type) => {
305
+ try {
306
+ // Check if stage has popup configuration
307
+ if (stage.popup?.enabled) {
308
+ setStagePopupData({ stage, type });
309
+ setShowStagePopup(true);
310
+ return;
311
+ }
312
+
313
+ // If no popup, proceed with normal stage update
314
+ await performStageUpdate(stage, type);
315
+ } catch (error) {
316
+ console.error(error);
317
+ }
318
+ };
319
+
320
+ const handleStagePopupConfirm = async (popupFormData) => {
321
+ try {
322
+ setShowStagePopup(false);
323
+
324
+ if (!stagePopupData) return;
325
+
326
+ const { stage, type } = stagePopupData;
327
+
328
+ // Proceed with normal stage update, but include popup form data
329
+ await performStageUpdate(stage, type, popupFormData);
330
+
331
+ setStagePopupData(null);
332
+ } catch (error) {
333
+ console.error('Stage popup confirm error:', error);
334
+ toast.error('Failed to update stage');
335
+ }
336
+ };
337
+
338
+ const handleStagePopupCancel = () => {
339
+ setShowStagePopup(false);
340
+ setStagePopupData(null);
341
+ };
342
+
343
+ const performStageUpdate = async (stage, type, additionalData = {}) => {
300
344
  try {
301
345
  const activeTab = subnav.find((s) => s.show === true);
302
346
 
@@ -307,7 +351,7 @@ function GenericDetail({
307
351
  setActiveStage(stage.id);
308
352
  } else if (type === 'stageCounter') {
309
353
  // Handle new stageCounter functionality
310
- console.log('handleStageUpdate called with stageCounter type:', { stage, data, stages });
354
+ console.log('performStageUpdate called with stageCounter type:', { stage, data, stages });
311
355
 
312
356
  const stageConfig = stages.stageConfig || activeTabConfig.stages?.stageConfig;
313
357
  if (!stageConfig?.toggleable || !stageConfig?.toggleConfig) {
@@ -319,13 +363,6 @@ function GenericDetail({
319
363
  const stageDataArray = data[stageConfig.key] || [];
320
364
  const currentStage = stageDataArray.find(s => s.id === stage.id);
321
365
 
322
- console.log('Stage data lookup:', {
323
- stageDataKey: stageConfig.key,
324
- stageDataArray,
325
- searchingForId: stage.id,
326
- currentStage
327
- });
328
-
329
366
  if (!currentStage) {
330
367
  console.warn('Stage not found in data');
331
368
  return;
@@ -337,26 +374,19 @@ function GenericDetail({
337
374
  const targetStatus = isActive
338
375
  ? stageConfig.toggleConfig.inactiveStatusValue
339
376
  : stageConfig.toggleConfig.activeStatusValue;
340
-
341
- console.log('Status toggle logic:', {
342
- currentStatus,
343
- activeStatusValue: stageConfig.toggleConfig.activeStatusValue,
344
- inactiveStatusValue: stageConfig.toggleConfig.inactiveStatusValue,
345
- isActive,
346
- targetStatus
347
- });
348
377
 
349
378
  // Build API URL
350
379
  let url = stageConfig.toggleConfig.updateUrl;
351
380
  url = url.replace('{id}', routeParams[stages.urlParam]);
352
381
  url = url.replace('{stage_id}', stage.id);
353
382
 
354
- // Make API call
383
+ // Make API call with additional data from popup
355
384
  const res = await CustomFetch(
356
385
  url,
357
386
  stageConfig.toggleConfig.updateMethod || 'PUT',
358
387
  {
359
388
  [stageConfig.toggleConfig.updateKey]: targetStatus,
389
+ ...additionalData
360
390
  }
361
391
  );
362
392
 
@@ -369,7 +399,7 @@ function GenericDetail({
369
399
  }
370
400
  } else if (type === 'legacy_toggle') {
371
401
  // Handle legacy toggle functionality - toggle between current stage and previous
372
- console.log('handleStageUpdate called with legacy_toggle type:', { stage, data, stages });
402
+ console.log('performStageUpdate called with legacy_toggle type:', { stage, data, stages });
373
403
 
374
404
  if (
375
405
  stages &&
@@ -392,21 +422,16 @@ function GenericDetail({
392
422
  // Stage is not completed, move to this stage
393
423
  targetStatus = stage.id;
394
424
  }
395
-
396
- console.log('Legacy toggle logic:', {
397
- currentStatus,
398
- clickedStageId: stage.id,
399
- targetStatus,
400
- isToggleBack: currentStatus >= stage.id
401
- });
402
425
 
403
426
  const url = `${stages.url}/${routeParams[stages.urlParam]}`;
404
427
 
428
+ // Include additional data from popup
405
429
  const res = await CustomFetch(
406
430
  url,
407
431
  stages.method || 'PUT',
408
432
  {
409
433
  [stages.key]: targetStatus,
434
+ ...additionalData
410
435
  }
411
436
  );
412
437
 
@@ -433,6 +458,7 @@ function GenericDetail({
433
458
 
434
459
  const res = await CustomFetch(url, 'POST', {
435
460
  [activeTabConfig.stages.key]: stage.id,
461
+ ...additionalData
436
462
  });
437
463
 
438
464
  if (res.data.error === '') {
@@ -457,6 +483,7 @@ function GenericDetail({
457
483
  stages.method || 'POST',
458
484
  {
459
485
  [stages.key]: stage.id,
486
+ ...additionalData
460
487
  }
461
488
  );
462
489
 
@@ -3130,6 +3157,16 @@ function GenericDetail({
3130
3157
  </div>
3131
3158
  </div>
3132
3159
  </Popup>
3160
+
3161
+ <StagePopupModal
3162
+ show={showStagePopup}
3163
+ stage={stagePopupData?.stage}
3164
+ popupConfig={stagePopupData?.stage?.popup}
3165
+ onConfirm={handleStagePopupConfirm}
3166
+ onCancel={handleStagePopupCancel}
3167
+ entityData={data}
3168
+ routeParams={routeParams}
3169
+ />
3133
3170
  </>
3134
3171
  );
3135
3172
  }
@@ -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
package/src/index.js CHANGED
@@ -63,6 +63,7 @@ import GenericQuote from './components/generic/GenericQuote';
63
63
  import GenericReport from './components/generic/GenericReport';
64
64
  import GenericSort from './components/generic/GenericSort';
65
65
  import NotificationList from './components/generic/NotificationList';
66
+ import StagePopupModal from './components/generic/StagePopupModal';
66
67
 
67
68
  /** Proposal Components */
68
69
  import ProposalTemplateSectionManager from './components/proposal/ProposalTemplateSectionManager';
@@ -129,6 +130,7 @@ export {
129
130
  SelectList,
130
131
  Select,
131
132
  SortableList,
133
+ StagePopupModal,
132
134
  Table,
133
135
  TableFilter,
134
136
  VariableInserter,