@visns-studio/visns-components 4.10.48 → 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 (61) hide show
  1. package/package.json +29 -29
  2. package/src/components/cms/Field.jsx +39 -63
  3. package/src/components/crm/AsyncSelect.jsx +121 -15
  4. package/src/components/crm/Breadcrumb.jsx +2 -18
  5. package/src/components/crm/DataGrid.jsx +51 -424
  6. package/src/components/crm/Field.jsx +185 -358
  7. package/src/components/crm/Form.jsx +104 -92
  8. package/src/components/crm/MultiSelect.jsx +66 -45
  9. package/src/components/crm/Navigation.jsx +9 -92
  10. package/src/components/crm/QuickAction.jsx +1 -0
  11. package/src/components/crm/auth/Login.jsx +9 -1
  12. package/src/components/crm/auth/Profile.jsx +4 -2
  13. package/src/components/crm/auth/styles/Login.module.scss +52 -82
  14. package/src/components/crm/auth/styles/Profile.module.scss +150 -44
  15. package/src/components/crm/auth/styles/Reset.module.scss +55 -37
  16. package/src/components/crm/auth/styles/Verify.module.scss +55 -76
  17. package/src/components/crm/generic/GenericAuth.jsx +12 -5
  18. package/src/components/crm/generic/GenericDashboard.jsx +30 -340
  19. package/src/components/crm/generic/GenericDetail.jsx +534 -86
  20. package/src/components/crm/generic/GenericDynamic.jsx +76 -94
  21. package/src/components/crm/generic/GenericEditableTable.jsx +407 -0
  22. package/src/components/crm/generic/GenericFormBuilder.jsx +19 -79
  23. package/src/components/crm/generic/GenericIndex.jsx +1 -2
  24. package/src/components/crm/generic/styles/AuditLog.module.scss +4 -37
  25. package/src/components/crm/generic/styles/AuditLogs.module.scss +0 -51
  26. package/src/components/crm/generic/styles/GenericDetail.module.scss +263 -244
  27. package/src/components/crm/generic/styles/GenericDynamic.module.scss +100 -198
  28. package/src/components/crm/generic/styles/GenericEditableTable.module.scss +178 -0
  29. package/src/components/crm/generic/styles/GenericFormBuilder.module.scss +63 -101
  30. package/src/components/crm/generic/styles/GenericIndex.module.scss +46 -66
  31. package/src/components/crm/generic/styles/GenericMain.module.scss +7 -7
  32. package/src/components/crm/generic/styles/GenericSort.module.scss +0 -36
  33. package/src/components/crm/generic/styles/NotificationList.module.scss +1 -32
  34. package/src/components/crm/styles/Autocomplete.module.scss +12 -20
  35. package/src/components/crm/styles/DataGrid.module.scss +28 -86
  36. package/src/components/crm/styles/Field.module.scss +72 -252
  37. package/src/components/crm/styles/Form.module.scss +141 -450
  38. package/src/components/crm/styles/Navigation.module.scss +356 -507
  39. package/src/components/crm/styles/Notification.module.scss +1 -0
  40. package/src/components/crm/styles/QrCode.module.scss +4 -18
  41. package/src/components/crm/styles/SwitchAccount.module.scss +0 -1
  42. package/src/components/crm/styles/TableFilter.module.scss +2 -1
  43. package/src/components/styles/global.css +164 -34
  44. package/src/index.js +0 -8
  45. package/src/components/crm/generic/styles/GenericDashboard.module.scss +0 -325
  46. package/src/components/crm/sketch/SketchField.jsx +0 -395
  47. package/src/components/crm/sketch/arrow.jsx +0 -108
  48. package/src/components/crm/sketch/circle.jsx +0 -75
  49. package/src/components/crm/sketch/default-tool.jsx +0 -16
  50. package/src/components/crm/sketch/fabrictool.jsx +0 -22
  51. package/src/components/crm/sketch/history.jsx +0 -144
  52. package/src/components/crm/sketch/json/config.json +0 -14
  53. package/src/components/crm/sketch/line.jsx +0 -64
  54. package/src/components/crm/sketch/pan.jsx +0 -48
  55. package/src/components/crm/sketch/pencil.jsx +0 -36
  56. package/src/components/crm/sketch/rectangle-label-object.jsx +0 -69
  57. package/src/components/crm/sketch/rectangle-label.jsx +0 -93
  58. package/src/components/crm/sketch/rectangle.jsx +0 -76
  59. package/src/components/crm/sketch/select.jsx +0 -16
  60. package/src/components/crm/sketch/tools.jsx +0 -11
  61. package/src/components/crm/sketch/utils.jsx +0 -67
@@ -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,28 +19,32 @@ 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',
39
46
  setting,
40
47
  setActiveNav = null,
41
- themeType,
42
48
  urlParam,
43
49
  userProfile,
44
50
  }) {
@@ -54,21 +60,156 @@ function GenericDetail({
54
60
 
55
61
  /** General States */
56
62
  const [activeStage, setActiveStage] = useState('');
63
+ const [activeTabConfig, setActiveTabConfig] = useState(null);
57
64
  const [config, setConfig] = useState({});
58
65
  const [data, setData] = useState({});
59
66
  const [dataReload, setDataReload] = useState(0);
67
+ const [passwordVisible, setPasswordVisible] = useState(false);
68
+ const [preloadedOptions, setPreloadedOptions] = useState({});
60
69
  const [rowsSelected, setRowsSelected] = useState({});
61
70
  const [subnav, setSubnav] = useState([]);
62
71
  const [total, setTotal] = useState(0);
63
72
  const [windowHeight, setWindowHeight] = useState(window.innerHeight);
64
73
 
65
- 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
+ );
66
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
+ };
207
+
208
+ /** General Functions */
67
209
  const togglePasswordVisibility = () => {
68
210
  setPasswordVisible(!passwordVisible);
69
211
  };
70
212
 
71
- /** General Functions */
72
213
  const handleStageUpdate = async (stage, type) => {
73
214
  try {
74
215
  const activeTab = subnav.find((s) => s.show === true);
@@ -96,6 +237,32 @@ function GenericDetail({
96
237
 
97
238
  if (res.data.error === '') {
98
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');
99
266
  } else {
100
267
  toast.error(res.data.error);
101
268
  }
@@ -107,6 +274,14 @@ function GenericDetail({
107
274
  }
108
275
  };
109
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
+
110
285
  const handleCheckboxUpdate = async () => {
111
286
  try {
112
287
  if (currentConfigRef.current.functions.checkboxUpdate) {
@@ -192,7 +367,6 @@ function GenericDetail({
192
367
  relationKey,
193
368
  size,
194
369
  truncateLength,
195
- checkboxLabel,
196
370
  } = item;
197
371
  const value =
198
372
  relation && relation.length > 0 && type !== 'total'
@@ -265,22 +439,16 @@ function GenericDetail({
265
439
  return '';
266
440
  }
267
441
 
268
- // Create a lookup map for checkbox labels if it exists
269
- const labelMap = checkboxLabel
270
- ? checkboxLabel.reduce((map, item) => {
271
- map[item.id] = item.label;
272
- return map;
273
- }, {})
274
- : {};
442
+ const formatKey = (key) => {
443
+ return key
444
+ .toLowerCase()
445
+ .split('_')
446
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
447
+ .join(' ');
448
+ };
275
449
 
276
450
  const keys = Object.keys(value)
277
- .filter(
278
- (key) =>
279
- (typeof value[key] === 'boolean' ||
280
- typeof value[key] === 'string') &&
281
- value[key] &&
282
- key !== 'notes'
283
- )
451
+ .filter((key) => value[key] && key !== 'notes')
284
452
  .sort((a, b) => {
285
453
  if (a === 'other') return 1;
286
454
  if (b === 'other') return -1;
@@ -288,44 +456,48 @@ function GenericDetail({
288
456
  });
289
457
 
290
458
  const formattedKeys = keys.map((key) => {
291
- // Use the label if available; otherwise, fallback to the formatted key
292
- let label = labelMap[key] || formatKey(key);
293
-
294
- // If the value is a string, append it directly to the label
295
- if (typeof value[key] === 'string') {
296
- return `${label}: ${value[key]}`;
459
+ let formattedKey = formatKey(key);
460
+ if (key === 'other') {
461
+ const otherValue = getValueFromData(
462
+ data,
463
+ [...relation],
464
+ `${id}_other`,
465
+ type
466
+ );
467
+
468
+ if (otherValue) {
469
+ formattedKey += `: ${otherValue}`;
470
+ }
297
471
  }
298
-
299
- // Return the label for boolean values
300
- return label;
472
+ return formattedKey;
301
473
  });
302
474
 
303
475
  return formattedKeys.join(', ');
304
476
  };
305
477
 
306
- // Helper function to format the key as a fallback
307
- const formatKey = (key) => {
308
- return key
309
- .toLowerCase()
310
- .split('_')
311
- .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
312
- .join(' ');
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);
313
489
  };
314
490
 
315
491
  const renderDate = () => formatDateValue(value);
316
492
 
317
493
  const renderDateTime = () => formatDateTimeValue(value);
318
494
 
319
- const renderFile = () => {
320
- // Check if value is a non-empty string and looks like a valid URL (optional)
321
- const isValidUrl = value && value.startsWith('http');
322
-
323
- return isValidUrl ? (
324
- <a href={value} target="_blank" rel="noopener noreferrer">
495
+ const renderFile = () =>
496
+ value ? (
497
+ <a href={value} target="_blank">
325
498
  Download
326
499
  </a>
327
500
  ) : null;
328
- };
329
501
 
330
502
  const renderExist = () => (truncatedValue ? 'Yes' : 'No');
331
503
 
@@ -403,6 +575,94 @@ function GenericDetail({
403
575
  }
404
576
  };
405
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
+
406
666
  // Render based on the item's type
407
667
  switch (type) {
408
668
  case 'boolean':
@@ -411,6 +671,8 @@ function GenericDetail({
411
671
  return renderCheckbox();
412
672
  case 'copyToClipboard':
413
673
  return renderCopyToClipboard();
674
+ case 'currency':
675
+ return renderCurrency();
414
676
  case 'date':
415
677
  return renderDate();
416
678
  case 'datetime':
@@ -425,6 +687,8 @@ function GenericDetail({
425
687
  return renderLatestNotes();
426
688
  case 'link':
427
689
  return renderLink();
690
+ case 'ocrTemplate':
691
+ return renderOcrTemplate(id);
428
692
  case 'options':
429
693
  return renderOptions();
430
694
  case 'password':
@@ -452,11 +716,82 @@ function GenericDetail({
452
716
 
453
717
  if (activeTabConfig && activeTabConfig.type) {
454
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
+ );
455
768
  case 'dropzone':
456
- const downloadFile = (id) => {
457
- window.open(
458
- `/ajax/files/download/${id}/${activeTabConfig.folder}`
459
- );
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
+ });
460
795
  };
461
796
 
462
797
  const deleteFile = (e, id, name) => {
@@ -538,9 +873,13 @@ function GenericDetail({
538
873
  key: response.key,
539
874
  bucket: response.bucket,
540
875
  filename: file.name,
876
+ file_name: file.name,
541
877
  filesize: file.size,
878
+ file_size: file.size,
542
879
  extension:
543
880
  response.extension,
881
+ file_extension:
882
+ response.extension,
544
883
  file_relationship:
545
884
  activeTabConfig.file_relationship,
546
885
  };
@@ -550,9 +889,30 @@ function GenericDetail({
550
889
  'PUT',
551
890
  requestBody,
552
891
  (res) => {
553
- setFiles(
554
- res.data.files
555
- );
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
+ }
556
916
  }
557
917
  );
558
918
  });
@@ -652,7 +1012,7 @@ function GenericDetail({
652
1012
  <li
653
1013
  onClick={() => {
654
1014
  downloadFile(
655
- a.id
1015
+ a
656
1016
  );
657
1017
  }}
658
1018
  key={'tf-' + b}
@@ -979,34 +1339,66 @@ function GenericDetail({
979
1339
  )}
980
1340
  </div>
981
1341
 
982
- {(activeTabConfig.form ||
983
- activeTabConfig.export) && (
1342
+ {activeTabConfig.form && (
984
1343
  <div className={styles.polActions}>
985
- {activeTabConfig.form && (
986
- <button
987
- className={styles.btn}
988
- onClick={() =>
989
- modalOpen(
990
- 'update',
991
- routeParams[
992
- urlParam
993
- ]
1344
+ {/* Always render the "Edit" button if activeTabConfig.form exists */}
1345
+ <button
1346
+ className={styles.btn}
1347
+ onClick={() =>
1348
+ modalOpen(
1349
+ 'update',
1350
+ routeParams[urlParam]
1351
+ )
1352
+ }
1353
+ >
1354
+ Edit
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>
994
1391
  )
995
- }
996
- >
997
- Edit
998
- </button>
999
- )}
1392
+ )}
1393
+
1394
+ {/* Check if export exists and render the "Export" button under the same polActions */}
1000
1395
  {activeTabConfig.export && (
1001
1396
  <button
1002
1397
  className={styles.btn}
1003
1398
  onClick={() =>
1004
- window.open
1005
- ? window.open(
1006
- `${activeTabConfig.export.url}/${routeParams[urlParam]}`,
1007
- '_blank'
1008
- )
1009
- : (window.location.href = `${activeTabConfig.export.url}/${routeParams[urlParam]}`)
1399
+ handleExport(
1400
+ activeTabConfig.export
1401
+ )
1010
1402
  }
1011
1403
  >
1012
1404
  Export
@@ -1244,6 +1636,16 @@ function GenericDetail({
1244
1636
  } else {
1245
1637
  return null;
1246
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
+ );
1247
1649
  default:
1248
1650
  return null;
1249
1651
  }
@@ -1253,6 +1655,49 @@ function GenericDetail({
1253
1655
  };
1254
1656
 
1255
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
+
1256
1701
  useEffect(() => {
1257
1702
  const handleResize = () => {
1258
1703
  setWindowHeight(window.innerHeight);
@@ -1374,6 +1819,7 @@ function GenericDetail({
1374
1819
  });
1375
1820
  }
1376
1821
  break;
1822
+ case 'scheduling':
1377
1823
  case 'overview':
1378
1824
  setConfig({});
1379
1825
  if (activeTabConfig.form) {
@@ -1414,8 +1860,17 @@ function GenericDetail({
1414
1860
 
1415
1861
  tabs.forEach((tab) => {
1416
1862
  if (tab.type && tab.type === 'dropzone') {
1417
- if (res.files) {
1418
- 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
+ }
1419
1874
  }
1420
1875
  }
1421
1876
  });
@@ -1491,10 +1946,6 @@ function GenericDetail({
1491
1946
  <div className={styles.oppstatus}>
1492
1947
  <ul className={styles.opppath}>
1493
1948
  {stages.data.map((stage) => {
1494
- if (!stage.id) {
1495
- return null; // Skip rendering this stage
1496
- }
1497
-
1498
1949
  const isStageComplete =
1499
1950
  data[stages.key] >= stage.id;
1500
1951
 
@@ -1536,13 +1987,7 @@ function GenericDetail({
1536
1987
 
1537
1988
  <div className={styles.grid}>
1538
1989
  <div className={styles.grid__subrow}>
1539
- <div
1540
- className={
1541
- themeType === 'alternate'
1542
- ? styles.grid__subnav__alternate
1543
- : styles.grid__subnav
1544
- }
1545
- >
1990
+ <div className={styles.grid__subnav}>
1546
1991
  <TableFilter filters={subnav} setFilters={setSubnav} />
1547
1992
  </div>
1548
1993
  <div className={styles.grid__subcontent}>
@@ -1565,6 +2010,9 @@ function GenericDetail({
1565
2010
  style={userProfile?.settings?.style || {}}
1566
2011
  updateForm={setFormData}
1567
2012
  userProfile={userProfile}
2013
+ paramValue={
2014
+ routeParams[urlParam] ? routeParams[urlParam] : 0
2015
+ }
1568
2016
  />
1569
2017
  </Popup>
1570
2018
  </>