@visns-studio/visns-components 5.12.14 → 5.12.16

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.14",
90
+ "version": "5.12.16",
91
91
  "description": "Various packages to assist in the development of our Custom Applications.",
92
92
  "main": "src/index.js",
93
93
  "files": [
@@ -16,48 +16,78 @@ const CategorizedDropZone = ({
16
16
  categoriesWhere = [],
17
17
  entityData = {},
18
18
  routeParams = {},
19
- fetchData = null
19
+ fetchData = null,
20
20
  }) => {
21
+ useEffect(() => {
22
+ // Initialize component
23
+ }, []);
24
+
25
+ // Helper function to get nested object value using dot notation
26
+ const getNestedValue = (obj, path) => {
27
+ return path.split('.').reduce((current, key) => {
28
+ return current && current[key] !== undefined ? current[key] : null;
29
+ }, obj);
30
+ };
31
+
32
+ // Helper function to set nested object value using dot notation
33
+ const setNestedValue = (obj, path, value) => {
34
+ const keys = path.split('.');
35
+ const lastKey = keys.pop();
36
+ const target = keys.reduce((current, key) => {
37
+ if (!current[key]) current[key] = {};
38
+ return current[key];
39
+ }, obj);
40
+ target[lastKey] = value;
41
+ };
21
42
  const [activeCategory, setActiveCategory] = useState(null);
22
43
  const [categorizedFiles, setCategorizedFiles] = useState({});
23
44
  const [dynamicCategories, setDynamicCategories] = useState([]);
24
45
  const [loadingCategories, setLoadingCategories] = useState(false);
25
46
 
26
47
  // Get final categories (dynamic or static)
27
- const finalCategories = dynamicCategories.length > 0 ? dynamicCategories : categories;
48
+ const finalCategories =
49
+ dynamicCategories.length > 0 ? dynamicCategories : categories;
28
50
 
29
51
  // Fetch categories from API if categoriesUrl is provided
30
52
  useEffect(() => {
31
53
  if (categoriesUrl) {
32
54
  setLoadingCategories(true);
33
-
55
+
34
56
  const requestData = {};
35
57
  if (categoriesWhere && categoriesWhere.length > 0) {
36
58
  requestData.where = categoriesWhere;
37
59
  }
38
60
 
39
61
  CustomFetch(categoriesUrl, 'POST', requestData)
40
- .then(response => {
62
+ .then((response) => {
41
63
  if (response.data && !response.data.error) {
42
- const categoriesData = Array.isArray(response.data.data) ? response.data.data :
43
- Array.isArray(response.data) ? response.data : [];
44
-
64
+ const categoriesData = Array.isArray(response.data.data)
65
+ ? response.data.data
66
+ : Array.isArray(response.data)
67
+ ? response.data
68
+ : [];
69
+
45
70
  // Transform API data to match expected format
46
- const transformedCategories = categoriesData.map(cat => ({
47
- id: cat.slug || cat.id,
48
- label: cat.name || cat.label,
49
- description: cat.description || ''
50
- }));
51
-
71
+ const transformedCategories = categoriesData.map(
72
+ (cat) => ({
73
+ id: cat.slug || cat.id,
74
+ label: cat.name || cat.label,
75
+ description: cat.description || '',
76
+ })
77
+ );
78
+
52
79
  setDynamicCategories(transformedCategories);
53
-
80
+
54
81
  // Set first category as active if none selected
55
- if (!activeCategory && transformedCategories.length > 0) {
82
+ if (
83
+ !activeCategory &&
84
+ transformedCategories.length > 0
85
+ ) {
56
86
  setActiveCategory(transformedCategories[0].id);
57
87
  }
58
88
  }
59
89
  })
60
- .catch(error => {
90
+ .catch((error) => {
61
91
  console.error('Failed to load categories:', error);
62
92
  })
63
93
  .finally(() => {
@@ -70,76 +100,108 @@ const CategorizedDropZone = ({
70
100
  }, [categoriesUrl, categoriesWhere, categories, activeCategory]);
71
101
 
72
102
  useEffect(() => {
103
+ const currentFiles = getNestedValue(entityData, dataKey);
73
104
  console.log('CategorizedDropZone: Initializing files', {
74
105
  finalCategories,
75
- entityData: entityData[dataKey],
76
- dataKey
106
+ entityData: currentFiles,
107
+ dataKey,
77
108
  });
78
-
109
+
79
110
  // Initialize categorized files structure
80
111
  const initialCategorizedFiles = {};
81
- finalCategories.forEach(category => {
112
+ finalCategories.forEach((category) => {
82
113
  initialCategorizedFiles[category.id] = [];
83
114
  });
84
-
115
+
85
116
  // If there are existing files, categorize them based on file_description
86
- if (entityData[dataKey] && Array.isArray(entityData[dataKey])) {
87
- console.log('CategorizedDropZone: Processing files', entityData[dataKey]);
88
-
89
- entityData[dataKey].forEach(file => {
117
+ if (currentFiles && Array.isArray(currentFiles)) {
118
+ console.log('CategorizedDropZone: Processing files', currentFiles);
119
+
120
+ currentFiles.forEach((file) => {
90
121
  console.log('CategorizedDropZone: Processing file', {
91
122
  file_name: file.file_name,
92
123
  file_description: file.file_description,
93
- id: file.id
124
+ id: file.id,
94
125
  });
95
-
96
- const category = getCategoryFromDescription(file.file_description);
97
- console.log('CategorizedDropZone: Category extracted', category);
98
-
126
+
127
+ const category = getCategoryFromDescription(
128
+ file.file_description
129
+ );
130
+ console.log(
131
+ 'CategorizedDropZone: Category extracted',
132
+ category
133
+ );
134
+
99
135
  if (category && initialCategorizedFiles[category]) {
100
136
  initialCategorizedFiles[category].push(file);
101
- console.log(`CategorizedDropZone: Added file ${file.file_name} to category ${category}`);
137
+ console.log(
138
+ `CategorizedDropZone: Added file ${file.file_name} to category ${category}`
139
+ );
102
140
  } else {
103
- console.log(`CategorizedDropZone: No category match for file ${file.file_name}`, {
104
- extractedCategory: category,
105
- availableCategories: Object.keys(initialCategorizedFiles)
106
- });
141
+ // For uncategorized files, add them to the first available category as fallback
142
+ if (finalCategories.length > 0) {
143
+ const firstCategory = finalCategories[0].id;
144
+ initialCategorizedFiles[firstCategory].push(file);
145
+ console.log(
146
+ `CategorizedDropZone: Added uncategorized file ${file.file_name} to first category ${firstCategory}`
147
+ );
148
+ } else {
149
+ console.log(
150
+ `CategorizedDropZone: No category match for file ${file.file_name}`,
151
+ {
152
+ extractedCategory: category,
153
+ availableCategories: Object.keys(
154
+ initialCategorizedFiles
155
+ ),
156
+ }
157
+ );
158
+ }
107
159
  }
108
160
  });
109
161
  }
110
-
111
- console.log('CategorizedDropZone: Final categorized files', initialCategorizedFiles);
162
+
163
+ console.log(
164
+ 'CategorizedDropZone: Final categorized files',
165
+ initialCategorizedFiles
166
+ );
112
167
  setCategorizedFiles(initialCategorizedFiles);
113
168
  }, [entityData, dataKey, finalCategories]);
114
169
 
115
170
  const getCategoryFromDescription = (description) => {
116
171
  if (!description) return null;
117
-
172
+
118
173
  // Check if the description contains a category identifier (handles both slugs and numeric IDs)
119
174
  const categoryMatch = description.match(/^category:([^-\s]+)/i);
120
175
  if (categoryMatch) {
121
176
  const extractedId = categoryMatch[1];
122
-
177
+
123
178
  // First, try to find by exact ID match
124
- const exactMatch = finalCategories.find(cat => cat.id === extractedId);
179
+ const exactMatch = finalCategories.find(
180
+ (cat) => cat.id === extractedId
181
+ );
125
182
  if (exactMatch) return extractedId;
126
-
183
+
127
184
  // If no exact match, try to find by numeric ID if extracted ID is numeric
128
185
  if (/^\d+$/.test(extractedId)) {
129
- const numericMatch = finalCategories.find(cat => cat.id == extractedId);
186
+ const numericMatch = finalCategories.find(
187
+ (cat) => cat.id == extractedId
188
+ );
130
189
  if (numericMatch) return numericMatch.id;
131
190
  }
132
-
191
+
133
192
  // Log for debugging
134
193
  console.log('Category extraction debug:', {
135
194
  description,
136
195
  extractedId,
137
- availableCategories: finalCategories.map(c => ({ id: c.id, label: c.label }))
196
+ availableCategories: finalCategories.map((c) => ({
197
+ id: c.id,
198
+ label: c.label,
199
+ })),
138
200
  });
139
201
  }
140
-
202
+
141
203
  // Fallback: check if description matches any category label
142
- const matchedCategory = finalCategories.find(cat =>
204
+ const matchedCategory = finalCategories.find((cat) =>
143
205
  description.toLowerCase().includes(cat.label.toLowerCase())
144
206
  );
145
207
  return matchedCategory?.id || null;
@@ -155,30 +217,40 @@ const CategorizedDropZone = ({
155
217
 
156
218
  const handleFileUpload = (files) => {
157
219
  // Add category prefix to file descriptions
158
- const activeCategoryData = finalCategories.find(cat => cat.id === activeCategory);
220
+ const activeCategoryData = finalCategories.find(
221
+ (cat) => cat.id === activeCategory
222
+ );
159
223
  const categoryPrefix = `category:${activeCategory}`;
160
-
224
+
161
225
  // Update the files with category information
162
- const updatedFiles = files.map(file => ({
226
+ const updatedFiles = files.map((file) => ({
163
227
  ...file,
164
- file_description: file.file_description ?
165
- `${categoryPrefix} - ${file.file_description}` :
166
- `${categoryPrefix} - ${activeCategoryData?.description || activeCategoryData?.label}`
228
+ file_description: file.file_description
229
+ ? `${categoryPrefix} - ${file.file_description}`
230
+ : `${categoryPrefix} - ${
231
+ activeCategoryData?.description ||
232
+ activeCategoryData?.label
233
+ }`,
167
234
  }));
168
-
235
+
169
236
  // Update the categorized files state
170
- setCategorizedFiles(prev => ({
237
+ setCategorizedFiles((prev) => ({
171
238
  ...prev,
172
- [activeCategory]: [...(prev[activeCategory] || []), ...updatedFiles]
239
+ [activeCategory]: [
240
+ ...(prev[activeCategory] || []),
241
+ ...updatedFiles,
242
+ ],
173
243
  }));
174
244
  };
175
245
 
176
246
  const handleFileDelete = (fileId) => {
177
247
  // Remove file from the appropriate category
178
- setCategorizedFiles(prev => {
248
+ setCategorizedFiles((prev) => {
179
249
  const updated = { ...prev };
180
- Object.keys(updated).forEach(categoryId => {
181
- updated[categoryId] = updated[categoryId].filter(file => file.id !== fileId);
250
+ Object.keys(updated).forEach((categoryId) => {
251
+ updated[categoryId] = updated[categoryId].filter(
252
+ (file) => file.id !== fileId
253
+ );
182
254
  });
183
255
  return updated;
184
256
  });
@@ -186,57 +258,65 @@ const CategorizedDropZone = ({
186
258
 
187
259
  const handleFileUpdate = (updatedFile) => {
188
260
  // Update file in the appropriate category
189
- const newCategory = getCategoryFromDescription(updatedFile.file_description);
190
-
191
- setCategorizedFiles(prev => {
261
+ const newCategory = getCategoryFromDescription(
262
+ updatedFile.file_description
263
+ );
264
+
265
+ setCategorizedFiles((prev) => {
192
266
  const updated = { ...prev };
193
-
267
+
194
268
  // Remove from old category
195
- Object.keys(updated).forEach(categoryId => {
196
- updated[categoryId] = updated[categoryId].filter(file => file.id !== updatedFile.id);
269
+ Object.keys(updated).forEach((categoryId) => {
270
+ updated[categoryId] = updated[categoryId].filter(
271
+ (file) => file.id !== updatedFile.id
272
+ );
197
273
  });
198
-
274
+
199
275
  // Add to new category
200
276
  if (newCategory && updated[newCategory]) {
201
277
  updated[newCategory].push(updatedFile);
202
278
  }
203
-
279
+
204
280
  return updated;
205
281
  });
206
282
  };
207
283
 
208
284
  // Prepare entity data for the current category
209
- const currentCategoryEntityData = {
210
- ...entityData,
211
- [dataKey]: getCurrentCategoryFiles()
212
- };
285
+ const currentCategoryEntityData = JSON.parse(JSON.stringify(entityData));
286
+ setNestedValue(
287
+ currentCategoryEntityData,
288
+ dataKey,
289
+ getCurrentCategoryFiles()
290
+ );
213
291
 
214
292
  if (loadingCategories) {
215
293
  return (
216
294
  <div className={styles.categorizedDropZone}>
217
- <div className={styles.loadingState}>
218
- Loading categories...
219
- </div>
295
+ <div className={styles.loadingState}>Loading categories...</div>
220
296
  </div>
221
297
  );
222
298
  }
223
299
 
224
300
  if (finalCategories.length === 0) {
225
301
  // No categories available - show a simple dropzone without categorization
302
+ const currentFiles = getNestedValue(entityData, dataKey) || [];
226
303
  console.log('CategorizedDropZone: No categories, showing all files', {
227
304
  dataKey,
228
- files: entityData[dataKey] || [],
229
- entityData
305
+ files: currentFiles,
306
+ entityData,
230
307
  });
231
-
308
+
232
309
  return (
233
310
  <div className={styles.categorizedDropZone}>
234
311
  <div className={styles.noCategoryContent}>
235
312
  <DropZone
236
- fetchData={fetchData || (() => {
237
- console.log('Files updated (no categories)');
238
- })}
239
- files={entityData[dataKey] || []}
313
+ fetchData={
314
+ fetchData ||
315
+ (() => {
316
+ console.log('Files updated (no categories)');
317
+ })
318
+ }
319
+ files={currentFiles}
240
320
  settings={{
241
321
  url: url,
242
322
  method: method,
@@ -248,7 +328,7 @@ const CategorizedDropZone = ({
248
328
  model: entityData?.constructor?.name || 'Design',
249
329
  folder: folder,
250
330
  filetype: filetype,
251
- deleteUrl: deleteUrl
331
+ deleteUrl: deleteUrl,
252
332
  }}
253
333
  />
254
334
  </div>
@@ -259,36 +339,51 @@ const CategorizedDropZone = ({
259
339
  return (
260
340
  <div className={styles.categorizedDropZone}>
261
341
  <div className={styles.categoryTabs}>
262
- {finalCategories.map(category => (
342
+ {finalCategories.map((category) => (
263
343
  <button
264
344
  key={category.id}
265
- className={`${styles.categoryTab} ${activeCategory === category.id ? styles.active : ''}`}
345
+ className={`${styles.categoryTab} ${
346
+ activeCategory === category.id ? styles.active : ''
347
+ }`}
266
348
  onClick={() => handleCategoryChange(category.id)}
267
349
  >
268
350
  {category.label}
269
- {categorizedFiles[category.id] && categorizedFiles[category.id].length > 0 && (
270
- <span className={styles.fileCount}>
271
- ({categorizedFiles[category.id].length})
272
- </span>
273
- )}
351
+ {categorizedFiles[category.id] &&
352
+ categorizedFiles[category.id].length > 0 && (
353
+ <span className={styles.fileCount}>
354
+ ({categorizedFiles[category.id].length})
355
+ </span>
356
+ )}
274
357
  </button>
275
358
  ))}
276
359
  </div>
277
-
360
+
278
361
  <div className={styles.categoryContent}>
279
362
  {activeCategory && (
280
363
  <div className={styles.categoryPanel}>
281
- {finalCategories.find(cat => cat.id === activeCategory)?.description && (
364
+ {finalCategories.find(
365
+ (cat) => cat.id === activeCategory
366
+ )?.description && (
282
367
  <p className={styles.categoryDescription}>
283
- {finalCategories.find(cat => cat.id === activeCategory)?.description}
368
+ {
369
+ finalCategories.find(
370
+ (cat) => cat.id === activeCategory
371
+ )?.description
372
+ }
284
373
  </p>
285
374
  )}
286
-
375
+
287
376
  <DropZone
288
- fetchData={fetchData || (() => {
289
- // Default fetch function - refresh current category files
290
- console.log('Files updated for category:', activeCategory);
291
- })}
377
+ fetchData={
378
+ fetchData ||
379
+ (() => {
380
+ // Default fetch function - refresh current category files
381
+ console.log(
382
+ 'Files updated for category:',
383
+ activeCategory
384
+ );
385
+ })
386
+ }
292
387
  files={getCurrentCategoryFiles()}
293
388
  settings={{
294
389
  url: url,
@@ -296,9 +391,14 @@ const CategorizedDropZone = ({
296
391
  type: 'gallery',
297
392
  data: {
298
393
  file_relationship: file_relationship,
299
- file_description: `category:${activeCategory} - ${finalCategories.find(cat => cat.id === activeCategory)?.label || activeCategory}`,
394
+ file_description: `category:${activeCategory} - ${
395
+ finalCategories.find(
396
+ (cat) => cat.id === activeCategory
397
+ )?.label || activeCategory
398
+ }`,
300
399
  },
301
- model: entityData?.constructor?.name || 'Design'
400
+ model:
401
+ entityData?.constructor?.name || 'Design',
302
402
  }}
303
403
  />
304
404
  </div>
@@ -308,4 +408,4 @@ const CategorizedDropZone = ({
308
408
  );
309
409
  };
310
410
 
311
- export default CategorizedDropZone;
411
+ export default CategorizedDropZone;
@@ -144,16 +144,19 @@ const getAcceptTypes = (filetype) => {
144
144
  return {
145
145
  'application/pdf': ['.pdf'],
146
146
  'application/msword': ['.doc'],
147
- 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': ['.docx'],
147
+ 'application/vnd.openxmlformats-officedocument.wordprocessingml.document':
148
+ ['.docx'],
148
149
  'application/vnd.ms-excel': ['.xls'],
149
- 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': ['.xlsx'],
150
+ 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet':
151
+ ['.xlsx'],
150
152
  'application/vnd.ms-powerpoint': ['.ppt'],
151
- 'application/vnd.openxmlformats-officedocument.presentationml.presentation': ['.pptx'],
153
+ 'application/vnd.openxmlformats-officedocument.presentationml.presentation':
154
+ ['.pptx'],
152
155
  'text/plain': ['.txt'],
153
156
  'text/csv': ['.csv'],
154
157
  'application/zip': ['.zip'],
155
158
  'application/x-rar-compressed': ['.rar'],
156
- 'application/x-7z-compressed': ['.7z']
159
+ 'application/x-7z-compressed': ['.7z'],
157
160
  };
158
161
  case 'image':
159
162
  return {
@@ -163,7 +166,7 @@ const getAcceptTypes = (filetype) => {
163
166
  'image/webp': ['.webp'],
164
167
  'image/svg+xml': ['.svg'],
165
168
  'image/bmp': ['.bmp'],
166
- 'image/tiff': ['.tiff', '.tif']
169
+ 'image/tiff': ['.tiff', '.tif'],
167
170
  };
168
171
  default:
169
172
  return undefined; // Accept all file types
@@ -173,7 +176,7 @@ const getAcceptTypes = (filetype) => {
173
176
  // Helper function to get the appropriate icon based on file extension
174
177
  const getFileIcon = (filename) => {
175
178
  const extension = filename.split('.').pop().toLowerCase();
176
-
179
+
177
180
  switch (extension) {
178
181
  case 'pdf':
179
182
  return <FileText strokeWidth={2} size={20} color="#dc2626" />;
@@ -182,7 +185,9 @@ const getFileIcon = (filename) => {
182
185
  return <FileText strokeWidth={2} size={20} color="#2563eb" />;
183
186
  case 'xls':
184
187
  case 'xlsx':
185
- return <FileSpreadsheet strokeWidth={2} size={20} color="#16a34a" />;
188
+ return (
189
+ <FileSpreadsheet strokeWidth={2} size={20} color="#16a34a" />
190
+ );
186
191
  case 'ppt':
187
192
  case 'pptx':
188
193
  return <FileText strokeWidth={2} size={20} color="#ea580c" />;
@@ -211,11 +216,11 @@ const getFileIcon = (filename) => {
211
216
  // Helper function to format file size
212
217
  const formatFileSize = (bytes) => {
213
218
  if (!bytes || bytes === 0) return '';
214
-
219
+
215
220
  const k = 1024;
216
221
  const sizes = ['B', 'KB', 'MB', 'GB'];
217
222
  const i = Math.floor(Math.log(bytes) / Math.log(k));
218
-
223
+
219
224
  return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
220
225
  };
221
226
 
@@ -425,14 +430,14 @@ function GenericDetail({
425
430
  const handleStagePopupConfirm = async (popupFormData) => {
426
431
  try {
427
432
  setShowStagePopup(false);
428
-
433
+
429
434
  if (!stagePopupData) return;
430
-
435
+
431
436
  const { stage, type } = stagePopupData;
432
-
437
+
433
438
  // Proceed with normal stage update, but include popup form data
434
439
  await performStageUpdate(stage, type, popupFormData);
435
-
440
+
436
441
  setStagePopupData(null);
437
442
  } catch (error) {
438
443
  console.error('Stage popup confirm error:', error);
@@ -456,56 +461,76 @@ function GenericDetail({
456
461
  setActiveStage(stage.id);
457
462
  } else if (type === 'stageCounter') {
458
463
  // Handle new stageCounter functionality
459
- console.log('performStageUpdate called with stageCounter type:', { stage, data, stages });
460
-
461
- const stageConfig = stages.stageConfig || activeTabConfig.stages?.stageConfig;
462
- if (!stageConfig?.toggleable || !stageConfig?.toggleConfig) {
463
- console.warn('Stage is not configured for toggling', { stageConfig });
464
+ console.log(
465
+ 'performStageUpdate called with stageCounter type:',
466
+ { stage, data, stages }
467
+ );
468
+
469
+ const stageConfig =
470
+ stages.stageConfig ||
471
+ activeTabConfig.stages?.stageConfig;
472
+ if (
473
+ !stageConfig?.toggleable ||
474
+ !stageConfig?.toggleConfig
475
+ ) {
476
+ console.warn('Stage is not configured for toggling', {
477
+ stageConfig,
478
+ });
464
479
  return;
465
480
  }
466
481
 
467
482
  // Get current stage data
468
483
  const stageDataArray = data[stageConfig.key] || [];
469
- const currentStage = stageDataArray.find(s => s.id === stage.id);
470
-
484
+ const currentStage = stageDataArray.find(
485
+ (s) => s.id === stage.id
486
+ );
487
+
471
488
  if (!currentStage) {
472
489
  console.warn('Stage not found in data');
473
490
  return;
474
491
  }
475
492
 
476
493
  // Determine current and target status
477
- const currentStatus = currentStage[stageConfig.toggleConfig.statusField];
478
- const isActive = currentStatus === stageConfig.toggleConfig.activeStatusValue;
479
- const targetStatus = isActive
480
- ? stageConfig.toggleConfig.inactiveStatusValue
494
+ const currentStatus =
495
+ currentStage[stageConfig.toggleConfig.statusField];
496
+ const isActive =
497
+ currentStatus ===
498
+ stageConfig.toggleConfig.activeStatusValue;
499
+ const targetStatus = isActive
500
+ ? stageConfig.toggleConfig.inactiveStatusValue
481
501
  : stageConfig.toggleConfig.activeStatusValue;
482
502
 
483
503
  // Build API URL
484
504
  let url = stageConfig.toggleConfig.updateUrl;
485
505
  url = url.replace('{id}', routeParams[stages.urlParam]);
486
506
  url = url.replace('{stage_id}', stage.id);
487
-
507
+
488
508
  // Make API call with additional data from popup
489
509
  const res = await CustomFetch(
490
510
  url,
491
511
  stageConfig.toggleConfig.updateMethod || 'PUT',
492
512
  {
493
513
  [stageConfig.toggleConfig.updateKey]: targetStatus,
494
- ...additionalData
514
+ ...additionalData,
495
515
  }
496
516
  );
497
517
 
498
518
  if (res.data.error === '') {
499
519
  handleReload();
500
520
  const actionText = isActive ? 'disabled' : 'enabled';
501
- toast.success(`Stage "${stage.label}" ${actionText} successfully`);
521
+ toast.success(
522
+ `Stage "${stage.label}" ${actionText} successfully`
523
+ );
502
524
  } else {
503
525
  toast.error(res.data.error);
504
526
  }
505
527
  } else if (type === 'legacy_toggle') {
506
528
  // Handle legacy toggle functionality - toggle between current stage and previous
507
- console.log('performStageUpdate called with legacy_toggle type:', { stage, data, stages });
508
-
529
+ console.log(
530
+ 'performStageUpdate called with legacy_toggle type:',
531
+ { stage, data, stages }
532
+ );
533
+
509
534
  if (
510
535
  stages &&
511
536
  stages.url &&
@@ -517,7 +542,7 @@ function GenericDetail({
517
542
  ) {
518
543
  const currentStatus = data[stages.key];
519
544
  let targetStatus;
520
-
545
+
521
546
  // If clicking on the current stage (completed), move back one step
522
547
  // If clicking on a future stage, move to that stage
523
548
  if (currentStatus >= stage.id) {
@@ -528,7 +553,9 @@ function GenericDetail({
528
553
  targetStatus = stage.id;
529
554
  }
530
555
 
531
- const url = `${stages.url}/${routeParams[stages.urlParam]}`;
556
+ const url = `${stages.url}/${
557
+ routeParams[stages.urlParam]
558
+ }`;
532
559
 
533
560
  // Include additional data from popup
534
561
  const res = await CustomFetch(
@@ -536,14 +563,19 @@ function GenericDetail({
536
563
  stages.method || 'PUT',
537
564
  {
538
565
  [stages.key]: targetStatus,
539
- ...additionalData
566
+ ...additionalData,
540
567
  }
541
568
  );
542
569
 
543
570
  if (res.data.error === '') {
544
571
  handleReload();
545
- const actionText = currentStatus >= stage.id ? 'moved back from' : 'progressed to';
546
- toast.success(`Stage ${actionText} "${stage.label}" successfully`);
572
+ const actionText =
573
+ currentStatus >= stage.id
574
+ ? 'moved back from'
575
+ : 'progressed to';
576
+ toast.success(
577
+ `Stage ${actionText} "${stage.label}" successfully`
578
+ );
547
579
  } else {
548
580
  toast.error(res.data.error);
549
581
  }
@@ -563,7 +595,7 @@ function GenericDetail({
563
595
 
564
596
  const res = await CustomFetch(url, 'POST', {
565
597
  [activeTabConfig.stages.key]: stage.id,
566
- ...additionalData
598
+ ...additionalData,
567
599
  });
568
600
 
569
601
  if (res.data.error === '') {
@@ -588,7 +620,7 @@ function GenericDetail({
588
620
  stages.method || 'POST',
589
621
  {
590
622
  [stages.key]: stage.id,
591
- ...additionalData
623
+ ...additionalData,
592
624
  }
593
625
  );
594
626
 
@@ -940,29 +972,48 @@ function GenericDetail({
940
972
 
941
973
  const renderJson = () => {
942
974
  if (!value) return null;
943
-
975
+
944
976
  try {
945
- const jsonData = typeof value === 'string' ? JSON.parse(value) : value;
946
-
977
+ const jsonData =
978
+ typeof value === 'string' ? JSON.parse(value) : value;
979
+
947
980
  // Check if this looks like a question/answer format
948
- const isQuestionAnswerFormat = Object.values(jsonData).every(item =>
949
- item && typeof item === 'object' &&
950
- (item.hasOwnProperty('question') || item.hasOwnProperty('answer'))
981
+ const isQuestionAnswerFormat = Object.values(jsonData).every(
982
+ (item) =>
983
+ item &&
984
+ typeof item === 'object' &&
985
+ (item.hasOwnProperty('question') ||
986
+ item.hasOwnProperty('answer'))
951
987
  );
952
-
988
+
953
989
  if (isQuestionAnswerFormat) {
954
990
  return (
955
991
  <div className={styles.jsonQuestionAnswer}>
956
992
  {Object.entries(jsonData).map(([key, item]) => {
957
- if (!item || typeof item !== 'object') return null;
958
-
959
- const question = item.question || key.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
960
- const answer = item.answer !== undefined ?
961
- (typeof item.answer === 'boolean' ? (item.answer ? 'Yes' : 'No') : String(item.answer)) :
962
- 'N/A';
963
-
993
+ if (!item || typeof item !== 'object')
994
+ return null;
995
+
996
+ const question =
997
+ item.question ||
998
+ key
999
+ .replace(/_/g, ' ')
1000
+ .replace(/\b\w/g, (l) =>
1001
+ l.toUpperCase()
1002
+ );
1003
+ const answer =
1004
+ item.answer !== undefined
1005
+ ? typeof item.answer === 'boolean'
1006
+ ? item.answer
1007
+ ? 'Yes'
1008
+ : 'No'
1009
+ : String(item.answer)
1010
+ : 'N/A';
1011
+
964
1012
  return (
965
- <div key={key} className={styles.questionAnswerPair}>
1013
+ <div
1014
+ key={key}
1015
+ className={styles.questionAnswerPair}
1016
+ >
966
1017
  <div className={styles.question}>
967
1018
  <strong>{question}</strong>
968
1019
  </div>
@@ -975,34 +1026,51 @@ function GenericDetail({
975
1026
  </div>
976
1027
  );
977
1028
  }
978
-
1029
+
979
1030
  // Check if this is a simple key-value object
980
- const isSimpleKeyValue = Object.values(jsonData).every(val =>
981
- typeof val !== 'object' || val === null
1031
+ const isSimpleKeyValue = Object.values(jsonData).every(
1032
+ (val) => typeof val !== 'object' || val === null
982
1033
  );
983
-
1034
+
984
1035
  if (isSimpleKeyValue) {
985
1036
  return (
986
1037
  <div className={styles.jsonKeyValue}>
987
1038
  {Object.entries(jsonData).map(([key, val]) => (
988
1039
  <div key={key} className={styles.keyValuePair}>
989
- <strong>{key.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase())}:</strong> {
990
- val !== null && val !== undefined ?
991
- (typeof val === 'boolean' ? (val ? 'Yes' : 'No') : String(val)) :
992
- 'N/A'
993
- }
1040
+ <strong>
1041
+ {key
1042
+ .replace(/_/g, ' ')
1043
+ .replace(/\b\w/g, (l) =>
1044
+ l.toUpperCase()
1045
+ )}
1046
+ :
1047
+ </strong>{' '}
1048
+ {val !== null && val !== undefined
1049
+ ? typeof val === 'boolean'
1050
+ ? val
1051
+ ? 'Yes'
1052
+ : 'No'
1053
+ : String(val)
1054
+ : 'N/A'}
994
1055
  </div>
995
1056
  ))}
996
1057
  </div>
997
1058
  );
998
1059
  }
999
-
1060
+
1000
1061
  // For complex nested objects, fall back to formatted JSON
1001
- return <pre className={styles.jsonRaw}>{JSON.stringify(jsonData, null, 2)}</pre>;
1002
-
1062
+ return (
1063
+ <pre className={styles.jsonRaw}>
1064
+ {JSON.stringify(jsonData, null, 2)}
1065
+ </pre>
1066
+ );
1003
1067
  } catch (error) {
1004
1068
  // If parsing fails, show raw value
1005
- return <pre className={styles.jsonRaw}>{JSON.stringify(value, null, 2)}</pre>;
1069
+ return (
1070
+ <pre className={styles.jsonRaw}>
1071
+ {JSON.stringify(value, null, 2)}
1072
+ </pre>
1073
+ );
1006
1074
  }
1007
1075
  };
1008
1076
 
@@ -1608,7 +1676,9 @@ function GenericDetail({
1608
1676
  />
1609
1677
  </div>
1610
1678
  <Dropzone
1611
- accept={getAcceptTypes(activeTabConfig.filetype)}
1679
+ accept={getAcceptTypes(
1680
+ activeTabConfig.filetype
1681
+ )}
1612
1682
  onDrop={(acceptedFiles) => {
1613
1683
  const uploadFile = async (file) => {
1614
1684
  // Optimize image if it's an image file
@@ -1804,7 +1874,9 @@ function GenericDetail({
1804
1874
  }
1805
1875
  >
1806
1876
  {files &&
1807
- Array.isArray(files) &&
1877
+ Array.isArray(
1878
+ files
1879
+ ) &&
1808
1880
  files.map(
1809
1881
  (a, b) => {
1810
1882
  const imageSource =
@@ -1829,33 +1901,59 @@ function GenericDetail({
1829
1901
  alt={
1830
1902
  a.file_name
1831
1903
  }
1832
- onClick={(e) => {
1904
+ onClick={(
1905
+ e
1906
+ ) => {
1833
1907
  e.stopPropagation();
1834
1908
  e.preventDefault();
1835
-
1909
+
1836
1910
  // Prepare gallery data in the format expected by Lightbox
1837
- const galleryArray = files
1838
- .filter(file => {
1839
- const imageSource = file.base64_encoded_image || file.s3_url || file.file_url;
1840
- return imageSource;
1841
- })
1842
- .map((file) => ({
1843
- src: file.base64_encoded_image || file.s3_url || file.file_url,
1844
- caption: file.file_name,
1845
- width: 320,
1846
- height: 240,
1847
- }));
1911
+ const galleryArray =
1912
+ files
1913
+ .filter(
1914
+ (
1915
+ file
1916
+ ) => {
1917
+ const imageSource =
1918
+ file.base64_encoded_image ||
1919
+ file.s3_url ||
1920
+ file.file_url;
1921
+ return imageSource;
1922
+ }
1923
+ )
1924
+ .map(
1925
+ (
1926
+ file
1927
+ ) => ({
1928
+ src:
1929
+ file.base64_encoded_image ||
1930
+ file.s3_url ||
1931
+ file.file_url,
1932
+ caption:
1933
+ file.file_name,
1934
+ width: 320,
1935
+ height: 240,
1936
+ })
1937
+ );
1848
1938
 
1849
1939
  // Set gallery data for lightbox
1850
- setGalleryData(galleryArray);
1851
-
1940
+ setGalleryData(
1941
+ galleryArray
1942
+ );
1943
+
1852
1944
  // Set initial lightbox index
1853
- setLightboxIndex(b);
1854
-
1945
+ setLightboxIndex(
1946
+ b
1947
+ );
1948
+
1855
1949
  // Open lightbox directly
1856
- setLightboxOpen(true);
1950
+ setLightboxOpen(
1951
+ true
1952
+ );
1953
+ }}
1954
+ style={{
1955
+ cursor: 'pointer',
1857
1956
  }}
1858
- style={{ cursor: 'pointer' }}
1859
1957
  />
1860
1958
  <TrashCan
1861
1959
  onClick={(
@@ -1912,14 +2010,20 @@ function GenericDetail({
1912
2010
  );
1913
2011
  }}
1914
2012
  >
1915
- {getFileIcon(a.file_name)}
2013
+ {getFileIcon(
2014
+ a.file_name
2015
+ )}
1916
2016
  <div className="filename">
1917
2017
  <div className="file-name">
1918
- {a.file_name}
2018
+ {
2019
+ a.file_name
2020
+ }
1919
2021
  </div>
1920
2022
  {a.file_size && (
1921
2023
  <div className="file-size">
1922
- {formatFileSize(a.file_size)}
2024
+ {formatFileSize(
2025
+ a.file_size
2026
+ )}
1923
2027
  </div>
1924
2028
  )}
1925
2029
  </div>
@@ -2474,7 +2578,8 @@ function GenericDetail({
2474
2578
  processed[key] =
2475
2579
  routeParams[urlParam];
2476
2580
  } else if (
2477
- typeof value === 'object' && value !== null
2581
+ typeof value === 'object' &&
2582
+ value !== null
2478
2583
  ) {
2479
2584
  processed[key] =
2480
2585
  processUrlParams(value);
@@ -2512,7 +2617,9 @@ function GenericDetail({
2512
2617
  }
2513
2618
  >
2514
2619
  <Dropzone
2515
- accept={getAcceptTypes(activeTabConfig.filetype)}
2620
+ accept={getAcceptTypes(
2621
+ activeTabConfig.filetype
2622
+ )}
2516
2623
  onDrop={(
2517
2624
  acceptedFiles
2518
2625
  ) => {
@@ -2753,21 +2860,34 @@ function GenericDetail({
2753
2860
  // Replace URL parameters like {dataId} with actual values
2754
2861
  const replaceUrlParams = (url) => {
2755
2862
  if (!url) return url;
2756
- return url.replace(/{dataId}/g, routeParams[urlParam] || '');
2863
+ return url.replace(
2864
+ /{dataId}/g,
2865
+ routeParams[urlParam] || ''
2866
+ );
2757
2867
  };
2758
-
2868
+
2759
2869
  return (
2760
2870
  <CategorizedDropZone
2761
2871
  filetype={activeTabConfig.filetype}
2762
2872
  folder={activeTabConfig.folder}
2763
2873
  dataKey={activeTabConfig.key}
2764
- file_relationship={activeTabConfig.file_relationship}
2874
+ file_relationship={
2875
+ activeTabConfig.file_relationship
2876
+ }
2765
2877
  url={replaceUrlParams(activeTabConfig.url)}
2766
2878
  method={activeTabConfig.method}
2767
- deleteUrl={replaceUrlParams(activeTabConfig.deleteUrl)}
2768
- categories={activeTabConfig.categories || []}
2769
- categoriesUrl={activeTabConfig.categoriesUrl}
2770
- categoriesWhere={activeTabConfig.categoriesWhere}
2879
+ deleteUrl={replaceUrlParams(
2880
+ activeTabConfig.deleteUrl
2881
+ )}
2882
+ categories={
2883
+ activeTabConfig.categories || []
2884
+ }
2885
+ categoriesUrl={
2886
+ activeTabConfig.categoriesUrl
2887
+ }
2888
+ categoriesWhere={
2889
+ activeTabConfig.categoriesWhere
2890
+ }
2771
2891
  entityData={data}
2772
2892
  routeParams={routeParams}
2773
2893
  fetchData={handleReload}
@@ -2786,7 +2906,12 @@ function GenericDetail({
2786
2906
  );
2787
2907
  case 'custom':
2788
2908
  // Handle custom components
2789
- if (activeTabConfig.component === 'AssociationManager' || activeTabConfig.component === 'ClientAssociationManager') {
2909
+ if (
2910
+ activeTabConfig.component ===
2911
+ 'AssociationManager' ||
2912
+ activeTabConfig.component ===
2913
+ 'ClientAssociationManager'
2914
+ ) {
2790
2915
  return (
2791
2916
  <AssociationManager
2792
2917
  endpoints={activeTabConfig.endpoints}
@@ -2806,7 +2931,10 @@ function GenericDetail({
2806
2931
  />
2807
2932
  );
2808
2933
  }
2809
- if (activeTabConfig.component === 'ProposalTemplateSectionManager') {
2934
+ if (
2935
+ activeTabConfig.component ===
2936
+ 'ProposalTemplateSectionManager'
2937
+ ) {
2810
2938
  return (
2811
2939
  <ProposalTemplateSectionManager
2812
2940
  {...activeTabConfig.props}
@@ -2815,7 +2943,10 @@ function GenericDetail({
2815
2943
  />
2816
2944
  );
2817
2945
  }
2818
- if (activeTabConfig.component === 'ProposalTemplatePreview') {
2946
+ if (
2947
+ activeTabConfig.component ===
2948
+ 'ProposalTemplatePreview'
2949
+ ) {
2819
2950
  return (
2820
2951
  <ProposalTemplatePreview
2821
2952
  {...activeTabConfig.props}
@@ -2868,7 +2999,10 @@ function GenericDetail({
2868
2999
  // Update files when active tab changes for dropzone tabs
2869
3000
  useEffect(() => {
2870
3001
  if (activeTabConfig && activeTabConfig.type === 'dropzone' && data) {
2871
- if (activeTabConfig.file_relationship && data[activeTabConfig.file_relationship]) {
3002
+ if (
3003
+ activeTabConfig.file_relationship &&
3004
+ data[activeTabConfig.file_relationship]
3005
+ ) {
2872
3006
  setFiles(data[activeTabConfig.file_relationship]);
2873
3007
  } else if (data.files) {
2874
3008
  setFiles(data.files);
@@ -3178,43 +3312,86 @@ function GenericDetail({
3178
3312
  <ul className={styles.opppath}>
3179
3313
  {stages.data.map((stage) => {
3180
3314
  let isStageComplete = false;
3181
-
3315
+
3182
3316
  if (stages.type === 'stageCounter') {
3183
3317
  // For stageCounter, check individual stage status
3184
- const stageConfig = stages.stageConfig;
3185
- if (stageConfig && data[stageConfig.key]) {
3186
- const stageDataArray = data[stageConfig.key];
3187
- const currentStage = stageDataArray.find(s => s.id === stage.id);
3188
- if (currentStage && stageConfig.toggleConfig) {
3189
- const currentStatus = currentStage[stageConfig.toggleConfig.statusField];
3190
- isStageComplete = currentStatus === stageConfig.toggleConfig.activeStatusValue;
3191
-
3318
+ const stageConfig =
3319
+ stages.stageConfig;
3320
+ if (
3321
+ stageConfig &&
3322
+ data[stageConfig.key]
3323
+ ) {
3324
+ const stageDataArray =
3325
+ data[stageConfig.key];
3326
+ const currentStage =
3327
+ stageDataArray.find(
3328
+ (s) => s.id === stage.id
3329
+ );
3330
+ if (
3331
+ currentStage &&
3332
+ stageConfig.toggleConfig
3333
+ ) {
3334
+ const currentStatus =
3335
+ currentStage[
3336
+ stageConfig
3337
+ .toggleConfig
3338
+ .statusField
3339
+ ];
3340
+ isStageComplete =
3341
+ currentStatus ===
3342
+ stageConfig.toggleConfig
3343
+ .activeStatusValue;
3344
+
3192
3345
  // Debug logging
3193
- console.log('Stage completion check:', {
3194
- stageId: stage.id,
3195
- stageLabel: stage.label,
3196
- currentStatus,
3197
- activeStatusValue: stageConfig.toggleConfig.activeStatusValue,
3198
- isStageComplete
3199
- });
3346
+ console.log(
3347
+ 'Stage completion check:',
3348
+ {
3349
+ stageId: stage.id,
3350
+ stageLabel:
3351
+ stage.label,
3352
+ currentStatus,
3353
+ activeStatusValue:
3354
+ stageConfig
3355
+ .toggleConfig
3356
+ .activeStatusValue,
3357
+ isStageComplete,
3358
+ }
3359
+ );
3200
3360
  }
3201
3361
  }
3202
3362
  } else {
3203
3363
  // Legacy stage completion check
3204
- isStageComplete = data[stages.key] >= stage.id;
3364
+ isStageComplete =
3365
+ data[stages.key] >= stage.id;
3205
3366
  }
3206
3367
 
3207
3368
  // Get color for stageCounter type
3208
3369
  let stageColor = null;
3209
- if (stages.type === 'stageCounter' && stages.stageConfig?.stageColour) {
3210
- const stageDataArray = data[stages.stageConfig.key] || [];
3211
- const currentStage = stageDataArray.find(s => s.id === stage.id);
3212
- if (currentStage) {
3213
- const currentStatus = currentStage[stages.stageConfig.stageColour.key];
3214
- const colorOption = stages.stageConfig.stageColour.options?.find(
3215
- opt => opt.id === currentStatus
3370
+ if (
3371
+ stages.type === 'stageCounter' &&
3372
+ stages.stageConfig?.stageColour
3373
+ ) {
3374
+ const stageDataArray =
3375
+ data[stages.stageConfig.key] ||
3376
+ [];
3377
+ const currentStage =
3378
+ stageDataArray.find(
3379
+ (s) => s.id === stage.id
3216
3380
  );
3217
- stageColor = colorOption?.colour;
3381
+ if (currentStage) {
3382
+ const currentStatus =
3383
+ currentStage[
3384
+ stages.stageConfig
3385
+ .stageColour.key
3386
+ ];
3387
+ const colorOption =
3388
+ stages.stageConfig.stageColour.options?.find(
3389
+ (opt) =>
3390
+ opt.id ===
3391
+ currentStatus
3392
+ );
3393
+ stageColor =
3394
+ colorOption?.colour;
3218
3395
  }
3219
3396
  }
3220
3397
 
@@ -3226,11 +3403,21 @@ function GenericDetail({
3226
3403
  ? `${styles['opp-complete']}`
3227
3404
  : ''
3228
3405
  }
3229
- style={stageColor ? {
3230
- borderColor: stageColor,
3231
- color: isStageComplete ? '#fff' : stageColor,
3232
- backgroundColor: isStageComplete ? stageColor : 'transparent'
3233
- } : {}}
3406
+ style={
3407
+ stageColor
3408
+ ? {
3409
+ borderColor:
3410
+ stageColor,
3411
+ color: isStageComplete
3412
+ ? '#fff'
3413
+ : stageColor,
3414
+ backgroundColor:
3415
+ isStageComplete
3416
+ ? stageColor
3417
+ : 'transparent',
3418
+ }
3419
+ : {}
3420
+ }
3234
3421
  onClick={(e) => {
3235
3422
  e.preventDefault();
3236
3423
 
@@ -3240,15 +3427,27 @@ function GenericDetail({
3240
3427
  );
3241
3428
  }}
3242
3429
  >
3243
- <a style={stageColor ? {
3244
- color: isStageComplete ? '#fff' : stageColor
3245
- } : {}}>
3430
+ <a
3431
+ style={
3432
+ stageColor
3433
+ ? {
3434
+ color: isStageComplete
3435
+ ? '#fff'
3436
+ : stageColor,
3437
+ }
3438
+ : {}
3439
+ }
3440
+ >
3246
3441
  {stage.label}
3247
3442
  {isStageComplete && (
3248
3443
  <CircleCheck
3249
3444
  strokeWidth={2}
3250
3445
  size={16}
3251
- color={stageColor ? '#fff' : undefined}
3446
+ color={
3447
+ stageColor
3448
+ ? '#fff'
3449
+ : undefined
3450
+ }
3252
3451
  />
3253
3452
  )}
3254
3453
  </a>
@@ -2634,8 +2634,8 @@ const GenericGrid = ({
2634
2634
  );
2635
2635
  return (
2636
2636
  matchingOption || {
2637
- value: val,
2638
- label: val,
2637
+ value: typeof val === 'object' ? (val.id || val.value || val.key || val.code || val.slug || val.uuid || Object.keys(val)[0]) : val,
2638
+ label: typeof val === 'object' ? (val.name || val.label || val.title || val.text || val.description || val.display_name || val.displayName || val.full_name || val.fullName || val.username || val.email || val.code || val.slug || val.id || val.value || val.key || val.uuid || JSON.stringify(val)) : val,
2639
2639
  }
2640
2640
  );
2641
2641
  })