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

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 (27) hide show
  1. package/package.json +1 -3
  2. package/src/components/crm/AsyncSelect.jsx +15 -121
  3. package/src/components/crm/DataGrid.jsx +5 -25
  4. package/src/components/crm/Field.jsx +148 -174
  5. package/src/components/crm/Form.jsx +28 -41
  6. package/src/components/crm/MultiSelect.jsx +29 -96
  7. package/src/components/crm/Navigation.jsx +40 -2
  8. package/src/components/crm/QrCode.jsx +8 -12
  9. package/src/components/crm/auth/Login.jsx +1 -9
  10. package/src/components/crm/auth/styles/Login.module.scss +3 -4
  11. package/src/components/crm/auth/styles/Profile.module.scss +14 -56
  12. package/src/components/crm/generic/GenericAuth.jsx +2 -5
  13. package/src/components/crm/generic/GenericDetail.jsx +18 -509
  14. package/src/components/crm/generic/styles/AuditLog.module.scss +0 -4
  15. package/src/components/crm/generic/styles/GenericDetail.module.scss +121 -261
  16. package/src/components/crm/generic/styles/GenericDynamic.module.scss +67 -35
  17. package/src/components/crm/generic/styles/GenericFormBuilder.module.scss +43 -4
  18. package/src/components/crm/generic/styles/GenericIndex.module.scss +1 -1
  19. package/src/components/crm/styles/Autocomplete.module.scss +20 -12
  20. package/src/components/crm/styles/DataGrid.module.scss +3 -2
  21. package/src/components/crm/styles/Field.module.scss +13 -19
  22. package/src/components/crm/styles/Form.module.scss +5 -5
  23. package/src/components/crm/styles/Navigation.module.scss +61 -11
  24. package/src/components/crm/styles/QrCode.module.scss +0 -4
  25. package/src/components/styles/global.css +5 -95
  26. package/src/components/crm/generic/GenericEditableTable.jsx +0 -407
  27. package/src/components/crm/generic/styles/GenericEditableTable.module.scss +0 -178
@@ -1,8 +1,7 @@
1
1
  import '../../styles/global.css';
2
2
 
3
- import React, { useEffect, useRef, useState } from 'react';
3
+ import React, { useCallback, useEffect, useRef, useState } from 'react';
4
4
  import { Link, useParams } from 'react-router-dom';
5
- import { Calendar, momentLocalizer } from 'react-big-calendar';
6
5
  import moment from 'moment';
7
6
  import parse from 'html-react-parser';
8
7
  import Popup from 'reactjs-popup';
@@ -11,7 +10,6 @@ import Dropzone from 'react-dropzone';
11
10
  import { confirmAlert } from 'react-confirm-alert';
12
11
  import truncate from 'truncate';
13
12
  import { CopyToClipboard } from 'react-copy-to-clipboard';
14
- import { saveAs } from 'file-saver';
15
13
  import { toast } from 'react-toastify';
16
14
  import {
17
15
  CircleCheck,
@@ -19,27 +17,22 @@ import {
19
17
  EyeOpen,
20
18
  EyeSlashed,
21
19
  File,
22
- Pencil,
23
20
  TrashCan,
24
21
  } from 'akar-icons';
25
22
 
26
23
  import 'react-confirm-alert/src/react-confirm-alert.css';
27
- import 'react-big-calendar/lib/css/react-big-calendar.css';
28
24
 
29
25
  import Breadcrumb from '../Breadcrumb';
30
26
  import CustomFetch from '../Fetch';
31
27
  import Form from '../Form';
32
28
  import GenericDynamic from './GenericDynamic';
33
- import GenericEditableTable from './GenericEditableTable';
34
- import MultiSelect from '../MultiSelect';
35
29
  import QrCode from '../QrCode';
30
+ import SortableList from '../sorting/List';
36
31
  import Table from '../DataGrid';
37
32
  import TableFilter from '../TableFilter';
38
33
 
39
34
  import styles from './styles/GenericDetail.module.scss';
40
35
 
41
- const localizer = momentLocalizer(moment);
42
-
43
36
  function GenericDetail({
44
37
  extraUrlParam,
45
38
  layout = 'full',
@@ -60,156 +53,21 @@ function GenericDetail({
60
53
 
61
54
  /** General States */
62
55
  const [activeStage, setActiveStage] = useState('');
63
- const [activeTabConfig, setActiveTabConfig] = useState(null);
64
56
  const [config, setConfig] = useState({});
65
57
  const [data, setData] = useState({});
66
58
  const [dataReload, setDataReload] = useState(0);
67
- const [passwordVisible, setPasswordVisible] = useState(false);
68
- const [preloadedOptions, setPreloadedOptions] = useState({});
69
59
  const [rowsSelected, setRowsSelected] = useState({});
70
60
  const [subnav, setSubnav] = useState([]);
71
61
  const [total, setTotal] = useState(0);
72
62
  const [windowHeight, setWindowHeight] = useState(window.innerHeight);
73
63
 
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
- };
64
+ const [passwordVisible, setPasswordVisible] = useState(false);
207
65
 
208
- /** General Functions */
209
66
  const togglePasswordVisibility = () => {
210
67
  setPasswordVisible(!passwordVisible);
211
68
  };
212
69
 
70
+ /** General Functions */
213
71
  const handleStageUpdate = async (stage, type) => {
214
72
  try {
215
73
  const activeTab = subnav.find((s) => s.show === true);
@@ -237,32 +95,6 @@ function GenericDetail({
237
95
 
238
96
  if (res.data.error === '') {
239
97
  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');
266
98
  } else {
267
99
  toast.error(res.data.error);
268
100
  }
@@ -274,14 +106,6 @@ function GenericDetail({
274
106
  }
275
107
  };
276
108
 
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
-
285
109
  const handleCheckboxUpdate = async () => {
286
110
  try {
287
111
  if (currentConfigRef.current.functions.checkboxUpdate) {
@@ -475,19 +299,6 @@ function GenericDetail({
475
299
  return formattedKeys.join(', ');
476
300
  };
477
301
 
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
-
491
302
  const renderDate = () => formatDateValue(value);
492
303
 
493
304
  const renderDateTime = () => formatDateTimeValue(value);
@@ -575,94 +386,6 @@ function GenericDetail({
575
386
  }
576
387
  };
577
388
 
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
-
666
389
  // Render based on the item's type
667
390
  switch (type) {
668
391
  case 'boolean':
@@ -671,8 +394,6 @@ function GenericDetail({
671
394
  return renderCheckbox();
672
395
  case 'copyToClipboard':
673
396
  return renderCopyToClipboard();
674
- case 'currency':
675
- return renderCurrency();
676
397
  case 'date':
677
398
  return renderDate();
678
399
  case 'datetime':
@@ -687,8 +408,6 @@ function GenericDetail({
687
408
  return renderLatestNotes();
688
409
  case 'link':
689
410
  return renderLink();
690
- case 'ocrTemplate':
691
- return renderOcrTemplate(id);
692
411
  case 'options':
693
412
  return renderOptions();
694
413
  case 'password':
@@ -716,82 +435,11 @@ function GenericDetail({
716
435
 
717
436
  if (activeTabConfig && activeTabConfig.type) {
718
437
  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
- );
768
438
  case 'dropzone':
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
- });
439
+ const downloadFile = (id) => {
440
+ window.open(
441
+ `/ajax/files/download/${id}/${activeTabConfig.folder}`
442
+ );
795
443
  };
796
444
 
797
445
  const deleteFile = (e, id, name) => {
@@ -873,13 +521,9 @@ function GenericDetail({
873
521
  key: response.key,
874
522
  bucket: response.bucket,
875
523
  filename: file.name,
876
- file_name: file.name,
877
524
  filesize: file.size,
878
- file_size: file.size,
879
525
  extension:
880
526
  response.extension,
881
- file_extension:
882
- response.extension,
883
527
  file_relationship:
884
528
  activeTabConfig.file_relationship,
885
529
  };
@@ -889,30 +533,9 @@ function GenericDetail({
889
533
  'PUT',
890
534
  requestBody,
891
535
  (res) => {
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
- }
536
+ setFiles(
537
+ res.data.files
538
+ );
916
539
  }
917
540
  );
918
541
  });
@@ -1012,7 +635,7 @@ function GenericDetail({
1012
635
  <li
1013
636
  onClick={() => {
1014
637
  downloadFile(
1015
- a
638
+ a.id
1016
639
  );
1017
640
  }}
1018
641
  key={'tf-' + b}
@@ -1341,7 +964,6 @@ function GenericDetail({
1341
964
 
1342
965
  {activeTabConfig.form && (
1343
966
  <div className={styles.polActions}>
1344
- {/* Always render the "Edit" button if activeTabConfig.form exists */}
1345
967
  <button
1346
968
  className={styles.btn}
1347
969
  onClick={() =>
@@ -1353,57 +975,6 @@ function GenericDetail({
1353
975
  >
1354
976
  Edit
1355
977
  </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
- )}
1407
978
  </div>
1408
979
  )}
1409
980
  </>
@@ -1636,16 +1207,6 @@ function GenericDetail({
1636
1207
  } else {
1637
1208
  return null;
1638
1209
  }
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
- );
1649
1210
  default:
1650
1211
  return null;
1651
1212
  }
@@ -1655,49 +1216,6 @@ function GenericDetail({
1655
1216
  };
1656
1217
 
1657
1218
  /** 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
-
1701
1219
  useEffect(() => {
1702
1220
  const handleResize = () => {
1703
1221
  setWindowHeight(window.innerHeight);
@@ -1819,7 +1337,6 @@ function GenericDetail({
1819
1337
  });
1820
1338
  }
1821
1339
  break;
1822
- case 'scheduling':
1823
1340
  case 'overview':
1824
1341
  setConfig({});
1825
1342
  if (activeTabConfig.form) {
@@ -1860,17 +1377,8 @@ function GenericDetail({
1860
1377
 
1861
1378
  tabs.forEach((tab) => {
1862
1379
  if (tab.type && tab.type === 'dropzone') {
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
- }
1380
+ if (res.files) {
1381
+ setFiles(res.files);
1874
1382
  }
1875
1383
  }
1876
1384
  });
@@ -1946,6 +1454,10 @@ function GenericDetail({
1946
1454
  <div className={styles.oppstatus}>
1947
1455
  <ul className={styles.opppath}>
1948
1456
  {stages.data.map((stage) => {
1457
+ if (!stage.id) {
1458
+ return null; // Skip rendering this stage
1459
+ }
1460
+
1949
1461
  const isStageComplete =
1950
1462
  data[stages.key] >= stage.id;
1951
1463
 
@@ -2010,9 +1522,6 @@ function GenericDetail({
2010
1522
  style={userProfile?.settings?.style || {}}
2011
1523
  updateForm={setFormData}
2012
1524
  userProfile={userProfile}
2013
- paramValue={
2014
- routeParams[urlParam] ? routeParams[urlParam] : 0
2015
- }
2016
1525
  />
2017
1526
  </Popup>
2018
1527
  </>