@visns-studio/visns-components 5.0.0 → 5.0.1-8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/package.json +3 -1
  2. package/src/components/crm/AsyncSelect.jsx +121 -15
  3. package/src/components/crm/DataGrid.jsx +26 -5
  4. package/src/components/crm/Field.jsx +174 -148
  5. package/src/components/crm/Form.jsx +46 -33
  6. package/src/components/crm/MultiSelect.jsx +96 -29
  7. package/src/components/crm/Navigation.jsx +2 -81
  8. package/src/components/crm/QrCode.jsx +12 -8
  9. package/src/components/crm/auth/Login.jsx +9 -1
  10. package/src/components/crm/auth/Profile.jsx +4 -2
  11. package/src/components/crm/auth/styles/Login.module.scss +52 -82
  12. package/src/components/crm/auth/styles/Profile.module.scss +150 -44
  13. package/src/components/crm/auth/styles/Reset.module.scss +55 -37
  14. package/src/components/crm/auth/styles/Verify.module.scss +55 -76
  15. package/src/components/crm/generic/GenericAuth.jsx +17 -6
  16. package/src/components/crm/generic/GenericDetail.jsx +509 -18
  17. package/src/components/crm/generic/GenericEditableTable.jsx +407 -0
  18. package/src/components/crm/generic/styles/AuditLog.module.scss +4 -37
  19. package/src/components/crm/generic/styles/AuditLogs.module.scss +0 -51
  20. package/src/components/crm/generic/styles/GenericDetail.module.scss +263 -193
  21. package/src/components/crm/generic/styles/GenericDynamic.module.scss +100 -198
  22. package/src/components/crm/generic/styles/GenericEditableTable.module.scss +178 -0
  23. package/src/components/crm/generic/styles/GenericFormBuilder.module.scss +63 -96
  24. package/src/components/crm/generic/styles/GenericIndex.module.scss +46 -66
  25. package/src/components/crm/generic/styles/GenericMain.module.scss +7 -7
  26. package/src/components/crm/generic/styles/GenericSort.module.scss +0 -36
  27. package/src/components/crm/generic/styles/NotificationList.module.scss +1 -32
  28. package/src/components/crm/styles/Autocomplete.module.scss +12 -20
  29. package/src/components/crm/styles/DataGrid.module.scss +6 -89
  30. package/src/components/crm/styles/Field.module.scss +72 -42
  31. package/src/components/crm/styles/Form.module.scss +148 -379
  32. package/src/components/crm/styles/Navigation.module.scss +356 -501
  33. package/src/components/crm/styles/Notification.module.scss +1 -0
  34. package/src/components/crm/styles/QrCode.module.scss +4 -0
  35. package/src/components/crm/styles/SwitchAccount.module.scss +0 -1
  36. package/src/components/crm/styles/TableFilter.module.scss +2 -1
  37. package/src/components/styles/global.css +164 -34
@@ -1,7 +1,8 @@
1
1
  import '../../styles/global.css';
2
2
 
3
- import React, { useCallback, useEffect, useRef, useState } from 'react';
3
+ import React, { useEffect, useRef, useState } from 'react';
4
4
  import { Link, useParams } from 'react-router-dom';
5
+ import { Calendar, momentLocalizer } from 'react-big-calendar';
5
6
  import moment from 'moment';
6
7
  import parse from 'html-react-parser';
7
8
  import Popup from 'reactjs-popup';
@@ -10,6 +11,7 @@ import Dropzone from 'react-dropzone';
10
11
  import { confirmAlert } from 'react-confirm-alert';
11
12
  import truncate from 'truncate';
12
13
  import { CopyToClipboard } from 'react-copy-to-clipboard';
14
+ import { saveAs } from 'file-saver';
13
15
  import { toast } from 'react-toastify';
14
16
  import {
15
17
  CircleCheck,
@@ -17,22 +19,27 @@ import {
17
19
  EyeOpen,
18
20
  EyeSlashed,
19
21
  File,
22
+ Pencil,
20
23
  TrashCan,
21
24
  } from 'akar-icons';
22
25
 
23
26
  import 'react-confirm-alert/src/react-confirm-alert.css';
27
+ import 'react-big-calendar/lib/css/react-big-calendar.css';
24
28
 
25
29
  import Breadcrumb from '../Breadcrumb';
26
30
  import CustomFetch from '../Fetch';
27
31
  import Form from '../Form';
28
32
  import GenericDynamic from './GenericDynamic';
33
+ import GenericEditableTable from './GenericEditableTable';
34
+ import MultiSelect from '../MultiSelect';
29
35
  import QrCode from '../QrCode';
30
- import SortableList from '../sorting/List';
31
36
  import Table from '../DataGrid';
32
37
  import TableFilter from '../TableFilter';
33
38
 
34
39
  import styles from './styles/GenericDetail.module.scss';
35
40
 
41
+ const localizer = momentLocalizer(moment);
42
+
36
43
  function GenericDetail({
37
44
  extraUrlParam,
38
45
  layout = 'full',
@@ -53,21 +60,156 @@ function GenericDetail({
53
60
 
54
61
  /** General States */
55
62
  const [activeStage, setActiveStage] = useState('');
63
+ const [activeTabConfig, setActiveTabConfig] = useState(null);
56
64
  const [config, setConfig] = useState({});
57
65
  const [data, setData] = useState({});
58
66
  const [dataReload, setDataReload] = useState(0);
67
+ const [passwordVisible, setPasswordVisible] = useState(false);
68
+ const [preloadedOptions, setPreloadedOptions] = useState({});
59
69
  const [rowsSelected, setRowsSelected] = useState({});
60
70
  const [subnav, setSubnav] = useState([]);
61
71
  const [total, setTotal] = useState(0);
62
72
  const [windowHeight, setWindowHeight] = useState(window.innerHeight);
63
73
 
64
- const [passwordVisible, setPasswordVisible] = useState(false);
74
+ /** Ocr States */
75
+ const [ocrRowsSelected, setOcrRowsSelected] = useState('');
76
+
77
+ /** Ocr Template Functionalities */
78
+ const handleOcrFileChange = async (e) => {
79
+ try {
80
+ const id = e.target.getAttribute('data-id');
81
+
82
+ const formData = new FormData();
83
+ formData.append('file', e.target.files[0]);
84
+
85
+ // Show a loading toast when the file is being processed
86
+ const loadingToast = toast.loading(
87
+ 'Processing file... Please wait!'
88
+ );
89
+
90
+ const csrfToken = document
91
+ .querySelector('meta[name="csrf-token"]')
92
+ .getAttribute('content');
93
+ const response = await fetch('/ajax/ocr/analyzeFile', {
94
+ method: 'POST',
95
+ headers: { 'X-CSRF-TOKEN': csrfToken },
96
+ body: formData,
97
+ });
98
+
99
+ if (response.ok) {
100
+ const result = await response.json();
101
+ setData((prevState) => ({
102
+ ...prevState,
103
+ [id]: result,
104
+ }));
105
+ toast.update(loadingToast, {
106
+ render: 'File processed successfully!',
107
+ type: 'success',
108
+ isLoading: false,
109
+ autoClose: 3000,
110
+ });
111
+ } else {
112
+ toast.update(loadingToast, {
113
+ render: 'Error processing file.',
114
+ type: 'error',
115
+ isLoading: false,
116
+ autoClose: 3000,
117
+ });
118
+ }
119
+ } catch (error) {
120
+ console.error(error);
121
+ toast.update(loadingToast, {
122
+ render: 'Something went wrong. Please try again.',
123
+ type: 'error',
124
+ isLoading: false,
125
+ autoClose: 3000,
126
+ });
127
+ }
128
+ };
129
+
130
+ const handleOcrKeyChange = (e) => {
131
+ const {
132
+ value,
133
+ dataset: { id },
134
+ } = e.target;
135
+
136
+ let selectedKey = data[id].selectedKey || [];
137
+
138
+ if (selectedKey.includes(value)) {
139
+ selectedKey = selectedKey.filter((key) => key !== value);
140
+ } else {
141
+ selectedKey.push(value);
142
+ }
143
+
144
+ setData((prevState) => ({
145
+ ...prevState,
146
+ [id]: {
147
+ ...prevState[id],
148
+ selectedKey,
149
+ },
150
+ }));
151
+ };
152
+
153
+ const handleSelectedOcrKey = (value, action, id) => {
154
+ setOcrRowsSelected(value.value);
155
+
156
+ if (value.value) {
157
+ setData((prevState) => ({
158
+ ...prevState,
159
+ [id]: {
160
+ ...prevState[id],
161
+ dynamicFilename: `${prevState[id].dynamicFilename}_{{ ${value.value} }}`,
162
+ },
163
+ }));
164
+ }
165
+ };
166
+
167
+ const handleOcrInputChange = (e) => {
168
+ const {
169
+ name,
170
+ value,
171
+ dataset: { id },
172
+ } = e.target;
173
+
174
+ switch (name) {
175
+ case 'dynamicFilename':
176
+ setData((prevState) => ({
177
+ ...prevState,
178
+ [id]: {
179
+ ...prevState[id],
180
+ dynamicFilename: value,
181
+ },
182
+ }));
183
+ break;
184
+ }
185
+ };
186
+
187
+ const handleOcrTemplateSave = async (id) => {
188
+ try {
189
+ const res = await CustomFetch(
190
+ `${activeTabConfig.form.url}/${data.id}`,
191
+ 'PUT',
192
+ {
193
+ ...data,
194
+ }
195
+ );
196
+
197
+ if (res.data.error === '') {
198
+ toast.success('Data saved successfully');
199
+ handleReload();
200
+ } else {
201
+ toast.error(res.data.error);
202
+ }
203
+ } catch (error) {
204
+ console.error(error);
205
+ }
206
+ };
65
207
 
208
+ /** General Functions */
66
209
  const togglePasswordVisibility = () => {
67
210
  setPasswordVisible(!passwordVisible);
68
211
  };
69
212
 
70
- /** General Functions */
71
213
  const handleStageUpdate = async (stage, type) => {
72
214
  try {
73
215
  const activeTab = subnav.find((s) => s.show === true);
@@ -95,6 +237,32 @@ function GenericDetail({
95
237
 
96
238
  if (res.data.error === '') {
97
239
  handleReload();
240
+ toast.success('Stage updated successfully');
241
+ } else {
242
+ toast.error(res.data.error);
243
+ }
244
+ } else if (
245
+ stages &&
246
+ stages.url &&
247
+ stages.url !== '' &&
248
+ stages.key &&
249
+ stages.key !== '' &&
250
+ stages.urlParam &&
251
+ stages.urlParam !== ''
252
+ ) {
253
+ const url = `${stages.url}/${routeParams[urlParam]}`;
254
+
255
+ const res = await CustomFetch(
256
+ url,
257
+ stages.method || 'POST',
258
+ {
259
+ [stages.key]: stage.id,
260
+ }
261
+ );
262
+
263
+ if (res.data.error === '') {
264
+ handleReload();
265
+ toast.success('Stage updated successfully');
98
266
  } else {
99
267
  toast.error(res.data.error);
100
268
  }
@@ -106,6 +274,14 @@ function GenericDetail({
106
274
  }
107
275
  };
108
276
 
277
+ const handleExport = (config) => {
278
+ if (config.url && config.key) {
279
+ window.open(`${config.url}/${data[config.key]}`);
280
+ } else {
281
+ toast.error('Export URL not found');
282
+ }
283
+ };
284
+
109
285
  const handleCheckboxUpdate = async () => {
110
286
  try {
111
287
  if (currentConfigRef.current.functions.checkboxUpdate) {
@@ -299,6 +475,19 @@ function GenericDetail({
299
475
  return formattedKeys.join(', ');
300
476
  };
301
477
 
478
+ const renderCurrency = () => {
479
+ // Check if the value is a valid number
480
+ const parsedValue = parseFloat(value);
481
+ if (isNaN(parsedValue)) return ''; // Return empty string if invalid
482
+
483
+ const formatter = new Intl.NumberFormat('en-AU', {
484
+ style: 'currency',
485
+ currency: 'AUD',
486
+ });
487
+
488
+ return formatter.format(parsedValue);
489
+ };
490
+
302
491
  const renderDate = () => formatDateValue(value);
303
492
 
304
493
  const renderDateTime = () => formatDateTimeValue(value);
@@ -386,6 +575,94 @@ function GenericDetail({
386
575
  }
387
576
  };
388
577
 
578
+ const renderOcrTemplate = (id) => {
579
+ const ocrTemplateBuilder = data[id];
580
+
581
+ return (
582
+ <>
583
+ <input
584
+ type="file"
585
+ data-id={id}
586
+ onChange={handleOcrFileChange}
587
+ />
588
+
589
+ {ocrTemplateBuilder &&
590
+ ocrTemplateBuilder.DeducedData &&
591
+ ocrTemplateBuilder.DeducedData.length > 0 && (
592
+ <table className={styles.schedulingTable}>
593
+ <thead>
594
+ <tr>
595
+ <th></th>
596
+ <th>Key</th>
597
+ <th>Value</th>
598
+ </tr>
599
+ </thead>
600
+ <tbody>
601
+ {Object.entries(
602
+ ocrTemplateBuilder.DeducedData[0]
603
+ ).map(([key, value], index) => (
604
+ <tr key={`ocr-keys-${index}`}>
605
+ <td>
606
+ <input
607
+ type="checkbox"
608
+ name={key}
609
+ value={key}
610
+ data-id={id}
611
+ checked={
612
+ ocrTemplateBuilder?.selectedKey?.includes(
613
+ key
614
+ ) || false
615
+ }
616
+ onChange={
617
+ handleOcrKeyChange
618
+ }
619
+ />
620
+ </td>
621
+ <td>{key}</td>
622
+ <td>{value}</td>
623
+ </tr>
624
+ ))}
625
+ </tbody>
626
+ </table>
627
+ )}
628
+
629
+ {ocrTemplateBuilder &&
630
+ ocrTemplateBuilder.selectedKey &&
631
+ ocrTemplateBuilder.selectedKey.length > 0 && (
632
+ <>
633
+ <strong>Selected Keys</strong>
634
+ <MultiSelect
635
+ className=""
636
+ inputValue={ocrRowsSelected}
637
+ multi={false}
638
+ onChange={handleSelectedOcrKey}
639
+ options={ocrTemplateBuilder.selectedKey.map(
640
+ (a) => ({
641
+ label: a,
642
+ value: a,
643
+ })
644
+ )}
645
+ placeholder="Select a Key to use"
646
+ settings={{ id: id }}
647
+ style={{}}
648
+ isCreatable={false}
649
+ creatableConfig={{}}
650
+ />
651
+ <br />
652
+ <strong>File Name</strong>
653
+ <input
654
+ type="text"
655
+ value={data[id]?.dynamicFilename || ''}
656
+ data-id={id}
657
+ onChange={handleOcrInputChange}
658
+ name="dynamicFilename"
659
+ />
660
+ </>
661
+ )}
662
+ </>
663
+ );
664
+ };
665
+
389
666
  // Render based on the item's type
390
667
  switch (type) {
391
668
  case 'boolean':
@@ -394,6 +671,8 @@ function GenericDetail({
394
671
  return renderCheckbox();
395
672
  case 'copyToClipboard':
396
673
  return renderCopyToClipboard();
674
+ case 'currency':
675
+ return renderCurrency();
397
676
  case 'date':
398
677
  return renderDate();
399
678
  case 'datetime':
@@ -408,6 +687,8 @@ function GenericDetail({
408
687
  return renderLatestNotes();
409
688
  case 'link':
410
689
  return renderLink();
690
+ case 'ocrTemplate':
691
+ return renderOcrTemplate(id);
411
692
  case 'options':
412
693
  return renderOptions();
413
694
  case 'password':
@@ -435,11 +716,82 @@ function GenericDetail({
435
716
 
436
717
  if (activeTabConfig && activeTabConfig.type) {
437
718
  switch (activeTabConfig.type) {
719
+ case 'calendar':
720
+ const events =
721
+ data?.[activeTabConfig?.dataKey]
722
+ ?.map((event) => {
723
+ // Safely access keys using optional chaining and defaults
724
+ const keys =
725
+ activeTabConfig?.config?.keys || {};
726
+ const eventStart = event?.[keys.start]
727
+ ? moment(event[keys.start])
728
+ : null;
729
+ const eventEnd = event?.[keys.end]
730
+ ? moment(event[keys.end])
731
+ : null;
732
+
733
+ // Only include valid events
734
+ if (eventStart && eventEnd) {
735
+ return {
736
+ title:
737
+ event?.[keys.title] ||
738
+ 'Untitled Event', // Default title if not provided
739
+ start: eventStart,
740
+ end: eventEnd,
741
+ };
742
+ }
743
+
744
+ return null; // Filter out invalid events
745
+ })
746
+ ?.filter(Boolean) || [];
747
+
748
+ return (
749
+ <Calendar
750
+ localizer={localizer}
751
+ events={events}
752
+ startAccessor="start"
753
+ endAccessor="end"
754
+ style={{
755
+ height:
756
+ activeTabConfig.config.height ||
757
+ 500,
758
+ }}
759
+ views={
760
+ activeTabConfig.config.views || [
761
+ 'month',
762
+ 'week',
763
+ 'day',
764
+ ]
765
+ }
766
+ />
767
+ );
438
768
  case 'dropzone':
439
- const downloadFile = (id) => {
440
- window.open(
441
- `/ajax/files/download/${id}/${activeTabConfig.folder}`
442
- );
769
+ const downloadFile = (file) => {
770
+ const { id, file_url, file_name } = file;
771
+
772
+ // If file_url exists, use it for the download, else use the id to download
773
+ const urlToDownload =
774
+ file_url || `/ajax/files/download/${id}`;
775
+
776
+ // Use fetch to retrieve the file as a blob
777
+ fetch(urlToDownload, {
778
+ method: 'GET',
779
+ headers: {
780
+ 'Content-Type':
781
+ 'application/octet-stream', // Specify content type as binary
782
+ },
783
+ })
784
+ .then((response) => response.blob()) // Convert response to Blob
785
+ .then((blob) => {
786
+ // Use FileSaver to download the file with the specified name
787
+ saveAs(blob, file_name);
788
+ })
789
+ .catch((error) => {
790
+ console.error(
791
+ 'Error downloading the file:',
792
+ error
793
+ );
794
+ });
443
795
  };
444
796
 
445
797
  const deleteFile = (e, id, name) => {
@@ -521,9 +873,13 @@ function GenericDetail({
521
873
  key: response.key,
522
874
  bucket: response.bucket,
523
875
  filename: file.name,
876
+ file_name: file.name,
524
877
  filesize: file.size,
878
+ file_size: file.size,
525
879
  extension:
526
880
  response.extension,
881
+ file_extension:
882
+ response.extension,
527
883
  file_relationship:
528
884
  activeTabConfig.file_relationship,
529
885
  };
@@ -533,9 +889,30 @@ function GenericDetail({
533
889
  'PUT',
534
890
  requestBody,
535
891
  (res) => {
536
- setFiles(
537
- res.data.files
538
- );
892
+ if (
893
+ activeTabConfig.file_relationship &&
894
+ res.data[
895
+ activeTabConfig
896
+ .file_relationship
897
+ ]
898
+ ) {
899
+ setFiles(
900
+ res.data[
901
+ activeTabConfig
902
+ .file_relationship
903
+ ]
904
+ );
905
+ } else {
906
+ if (
907
+ res?.data
908
+ ?.files
909
+ ) {
910
+ setFiles(
911
+ res.data
912
+ .files
913
+ );
914
+ }
915
+ }
539
916
  }
540
917
  );
541
918
  });
@@ -635,7 +1012,7 @@ function GenericDetail({
635
1012
  <li
636
1013
  onClick={() => {
637
1014
  downloadFile(
638
- a.id
1015
+ a
639
1016
  );
640
1017
  }}
641
1018
  key={'tf-' + b}
@@ -964,6 +1341,7 @@ function GenericDetail({
964
1341
 
965
1342
  {activeTabConfig.form && (
966
1343
  <div className={styles.polActions}>
1344
+ {/* Always render the "Edit" button if activeTabConfig.form exists */}
967
1345
  <button
968
1346
  className={styles.btn}
969
1347
  onClick={() =>
@@ -975,6 +1353,57 @@ function GenericDetail({
975
1353
  >
976
1354
  Edit
977
1355
  </button>
1356
+
1357
+ {/* Check if sections contain "ocrTemplate" type and render the "Save" button under the same polActions */}
1358
+ {activeTabConfig.sections &&
1359
+ activeTabConfig.sections
1360
+ .length > 0 &&
1361
+ activeTabConfig.sections.map(
1362
+ (section, sectionIndex) =>
1363
+ section.content &&
1364
+ section.content.some(
1365
+ (item) =>
1366
+ item.type ===
1367
+ 'ocrTemplate'
1368
+ ) && (
1369
+ <button
1370
+ key={`save-button-${sectionIndex}`}
1371
+ className={
1372
+ styles.btn
1373
+ }
1374
+ onClick={() => {
1375
+ // Handle the specific "ocrTemplate" item
1376
+ const ocrItem =
1377
+ section.content.find(
1378
+ (
1379
+ item
1380
+ ) =>
1381
+ item.type ===
1382
+ 'ocrTemplate'
1383
+ );
1384
+ handleOcrTemplateSave(
1385
+ ocrItem.id
1386
+ ); // Now you can safely use ocrItem.id
1387
+ }}
1388
+ >
1389
+ Save
1390
+ </button>
1391
+ )
1392
+ )}
1393
+
1394
+ {/* Check if export exists and render the "Export" button under the same polActions */}
1395
+ {activeTabConfig.export && (
1396
+ <button
1397
+ className={styles.btn}
1398
+ onClick={() =>
1399
+ handleExport(
1400
+ activeTabConfig.export
1401
+ )
1402
+ }
1403
+ >
1404
+ Export
1405
+ </button>
1406
+ )}
978
1407
  </div>
979
1408
  )}
980
1409
  </>
@@ -1207,6 +1636,16 @@ function GenericDetail({
1207
1636
  } else {
1208
1637
  return null;
1209
1638
  }
1639
+ case 'scheduling':
1640
+ return (
1641
+ <GenericEditableTable
1642
+ schedulingConfig={activeTabConfig}
1643
+ data={data}
1644
+ dataId={routeParams[urlParam]}
1645
+ modalOpen={modalOpen}
1646
+ preloadedOptions={preloadedOptions}
1647
+ />
1648
+ );
1210
1649
  default:
1211
1650
  return null;
1212
1651
  }
@@ -1216,6 +1655,49 @@ function GenericDetail({
1216
1655
  };
1217
1656
 
1218
1657
  /** General Hooks */
1658
+ useEffect(() => {
1659
+ if (tabs && tabs.length > 0) {
1660
+ const activeTab = subnav.find((s) => s.show === true);
1661
+
1662
+ if (activeTab) {
1663
+ const newActiveTabConfig = tabs.find(
1664
+ (t) => t.id === activeTab.id
1665
+ );
1666
+ setActiveTabConfig(newActiveTabConfig); // Update state
1667
+ }
1668
+ }
1669
+ }, [subnav, tabs]);
1670
+
1671
+ useEffect(() => {
1672
+ const fetchDropdownOptions = async () => {
1673
+ if (activeTabConfig?.columns) {
1674
+ const ajaxColumns = activeTabConfig.columns.filter(
1675
+ (column) =>
1676
+ ['dropdown-ajax', 'multi-dropdown-ajax'].includes(
1677
+ column.type
1678
+ ) && column.url
1679
+ );
1680
+
1681
+ const options = {};
1682
+ for (const column of ajaxColumns) {
1683
+ try {
1684
+ const res = await CustomFetch(column.url, 'POST');
1685
+ options[column.id] = res.data.data || res.data || []; // Assume API returns options in the expected format
1686
+ } catch (error) {
1687
+ console.error(
1688
+ `Error fetching options for ${column.id}:`,
1689
+ error
1690
+ );
1691
+ }
1692
+ }
1693
+
1694
+ setPreloadedOptions(options);
1695
+ }
1696
+ };
1697
+
1698
+ fetchDropdownOptions();
1699
+ }, [activeTabConfig]); // Depend on activeTabConfig
1700
+
1219
1701
  useEffect(() => {
1220
1702
  const handleResize = () => {
1221
1703
  setWindowHeight(window.innerHeight);
@@ -1337,6 +1819,7 @@ function GenericDetail({
1337
1819
  });
1338
1820
  }
1339
1821
  break;
1822
+ case 'scheduling':
1340
1823
  case 'overview':
1341
1824
  setConfig({});
1342
1825
  if (activeTabConfig.form) {
@@ -1377,8 +1860,17 @@ function GenericDetail({
1377
1860
 
1378
1861
  tabs.forEach((tab) => {
1379
1862
  if (tab.type && tab.type === 'dropzone') {
1380
- if (res.files) {
1381
- setFiles(res.files);
1863
+ if (
1864
+ tab.file_relationship &&
1865
+ tab.file_relationship !== ''
1866
+ ) {
1867
+ if (res[tab.file_relationship]) {
1868
+ setFiles(res[tab.file_relationship]);
1869
+ }
1870
+ } else {
1871
+ if (res.files) {
1872
+ setFiles(res.files);
1873
+ }
1382
1874
  }
1383
1875
  }
1384
1876
  });
@@ -1454,10 +1946,6 @@ function GenericDetail({
1454
1946
  <div className={styles.oppstatus}>
1455
1947
  <ul className={styles.opppath}>
1456
1948
  {stages.data.map((stage) => {
1457
- if (!stage.id) {
1458
- return null; // Skip rendering this stage
1459
- }
1460
-
1461
1949
  const isStageComplete =
1462
1950
  data[stages.key] >= stage.id;
1463
1951
 
@@ -1522,6 +2010,9 @@ function GenericDetail({
1522
2010
  style={userProfile?.settings?.style || {}}
1523
2011
  updateForm={setFormData}
1524
2012
  userProfile={userProfile}
2013
+ paramValue={
2014
+ routeParams[urlParam] ? routeParams[urlParam] : 0
2015
+ }
1525
2016
  />
1526
2017
  </Popup>
1527
2018
  </>