@visns-studio/visns-components 5.0.13 → 5.0.15

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
@@ -75,7 +75,7 @@
75
75
  "react-dom": "^17.0.0 || ^18.0.0"
76
76
  },
77
77
  "name": "@visns-studio/visns-components",
78
- "version": "5.0.13",
78
+ "version": "5.0.15",
79
79
  "description": "Various packages to assist in the development of our Custom Applications.",
80
80
  "main": "src/index.js",
81
81
  "files": [
@@ -82,6 +82,7 @@ function Field({
82
82
  useEffect(() => {
83
83
  const fetchOptions = () => {
84
84
  let filter = {};
85
+
85
86
  if (settings.where?.length > 0) {
86
87
  filter = {
87
88
  where: settings.where,
@@ -29,6 +29,7 @@ import CustomFetch from '../Fetch';
29
29
  import Form from '../Form';
30
30
  import GenericDynamic from './GenericDynamic';
31
31
  import GenericEditableTable from './GenericEditableTable';
32
+ import MultiSelect from '../MultiSelect';
32
33
  import QrCode from '../QrCode';
33
34
  import Table from '../DataGrid';
34
35
  import TableFilter from '../TableFilter';
@@ -59,19 +60,152 @@ function GenericDetail({
59
60
  const [config, setConfig] = useState({});
60
61
  const [data, setData] = useState({});
61
62
  const [dataReload, setDataReload] = useState(0);
63
+ const [passwordVisible, setPasswordVisible] = useState(false);
62
64
  const [preloadedOptions, setPreloadedOptions] = useState({});
63
65
  const [rowsSelected, setRowsSelected] = useState({});
64
66
  const [subnav, setSubnav] = useState([]);
65
67
  const [total, setTotal] = useState(0);
66
68
  const [windowHeight, setWindowHeight] = useState(window.innerHeight);
67
69
 
68
- const [passwordVisible, setPasswordVisible] = useState(false);
70
+ /** Ocr States */
71
+ const [ocrRowsSelected, setOcrRowsSelected] = useState('');
72
+
73
+ /** Ocr Template Functionalities */
74
+ const handleOcrFileChange = async (e) => {
75
+ try {
76
+ const id = e.target.getAttribute('data-id');
77
+
78
+ const formData = new FormData();
79
+ formData.append('file', e.target.files[0]);
80
+
81
+ // Show a loading toast when the file is being processed
82
+ const loadingToast = toast.loading(
83
+ 'Processing file... Please wait!'
84
+ );
85
+
86
+ const csrfToken = document
87
+ .querySelector('meta[name="csrf-token"]')
88
+ .getAttribute('content');
89
+ const response = await fetch('/ajax/ocr/analyzeFile', {
90
+ method: 'POST',
91
+ headers: { 'X-CSRF-TOKEN': csrfToken },
92
+ body: formData,
93
+ });
69
94
 
95
+ if (response.ok) {
96
+ const result = await response.json();
97
+ setData((prevState) => ({
98
+ ...prevState,
99
+ [id]: result,
100
+ }));
101
+ toast.update(loadingToast, {
102
+ render: 'File processed successfully!',
103
+ type: 'success',
104
+ isLoading: false,
105
+ autoClose: 3000,
106
+ });
107
+ } else {
108
+ toast.update(loadingToast, {
109
+ render: 'Error processing file.',
110
+ type: 'error',
111
+ isLoading: false,
112
+ autoClose: 3000,
113
+ });
114
+ }
115
+ } catch (error) {
116
+ console.error(error);
117
+ toast.update(loadingToast, {
118
+ render: 'Something went wrong. Please try again.',
119
+ type: 'error',
120
+ isLoading: false,
121
+ autoClose: 3000,
122
+ });
123
+ }
124
+ };
125
+
126
+ const handleOcrKeyChange = (e) => {
127
+ const {
128
+ value,
129
+ dataset: { id },
130
+ } = e.target;
131
+
132
+ let selectedKey = data[id].selectedKey || [];
133
+
134
+ if (selectedKey.includes(value)) {
135
+ selectedKey = selectedKey.filter((key) => key !== value);
136
+ } else {
137
+ selectedKey.push(value);
138
+ }
139
+
140
+ setData((prevState) => ({
141
+ ...prevState,
142
+ [id]: {
143
+ ...prevState[id],
144
+ selectedKey,
145
+ },
146
+ }));
147
+ };
148
+
149
+ const handleSelectedOcrKey = (value, action, id) => {
150
+ setOcrRowsSelected(value.value);
151
+
152
+ if (value.value) {
153
+ setData((prevState) => ({
154
+ ...prevState,
155
+ [id]: {
156
+ ...prevState[id],
157
+ dynamicFilename: `${prevState[id].dynamicFilename}_{{ ${value.value} }}`,
158
+ },
159
+ }));
160
+ }
161
+ };
162
+
163
+ const handleOcrInputChange = (e) => {
164
+ const {
165
+ name,
166
+ value,
167
+ dataset: { id },
168
+ } = e.target;
169
+
170
+ switch (name) {
171
+ case 'dynamicFilename':
172
+ setData((prevState) => ({
173
+ ...prevState,
174
+ [id]: {
175
+ ...prevState[id],
176
+ dynamicFilename: value,
177
+ },
178
+ }));
179
+ break;
180
+ }
181
+ };
182
+
183
+ const handleOcrTemplateSave = async (id) => {
184
+ try {
185
+ const res = await CustomFetch(
186
+ `${activeTabConfig.form.url}/${data.id}`,
187
+ 'PUT',
188
+ {
189
+ ...data,
190
+ }
191
+ );
192
+
193
+ if (res.data.error === '') {
194
+ toast.success('Data saved successfully');
195
+ handleReload();
196
+ } else {
197
+ toast.error(res.data.error);
198
+ }
199
+ } catch (error) {
200
+ console.error(error);
201
+ }
202
+ };
203
+
204
+ /** General Functions */
70
205
  const togglePasswordVisibility = () => {
71
206
  setPasswordVisible(!passwordVisible);
72
207
  };
73
208
 
74
- /** General Functions */
75
209
  const handleStageUpdate = async (stage, type) => {
76
210
  try {
77
211
  const activeTab = subnav.find((s) => s.show === true);
@@ -437,6 +571,94 @@ function GenericDetail({
437
571
  }
438
572
  };
439
573
 
574
+ const renderOcrTemplate = (id) => {
575
+ const ocrTemplateBuilder = data[id];
576
+
577
+ return (
578
+ <>
579
+ <input
580
+ type="file"
581
+ data-id={id}
582
+ onChange={handleOcrFileChange}
583
+ />
584
+
585
+ {ocrTemplateBuilder &&
586
+ ocrTemplateBuilder.DeducedData &&
587
+ ocrTemplateBuilder.DeducedData.length > 0 && (
588
+ <table className={styles.schedulingTable}>
589
+ <thead>
590
+ <tr>
591
+ <th></th>
592
+ <th>Key</th>
593
+ <th>Value</th>
594
+ </tr>
595
+ </thead>
596
+ <tbody>
597
+ {Object.entries(
598
+ ocrTemplateBuilder.DeducedData[0]
599
+ ).map(([key, value], index) => (
600
+ <tr key={`ocr-keys-${index}`}>
601
+ <td>
602
+ <input
603
+ type="checkbox"
604
+ name={key}
605
+ value={key}
606
+ data-id={id}
607
+ checked={
608
+ ocrTemplateBuilder?.selectedKey?.includes(
609
+ key
610
+ ) || false
611
+ }
612
+ onChange={
613
+ handleOcrKeyChange
614
+ }
615
+ />
616
+ </td>
617
+ <td>{key}</td>
618
+ <td>{value}</td>
619
+ </tr>
620
+ ))}
621
+ </tbody>
622
+ </table>
623
+ )}
624
+
625
+ {ocrTemplateBuilder &&
626
+ ocrTemplateBuilder.selectedKey &&
627
+ ocrTemplateBuilder.selectedKey.length > 0 && (
628
+ <>
629
+ <strong>Selected Keys</strong>
630
+ <MultiSelect
631
+ className=""
632
+ inputValue={ocrRowsSelected}
633
+ multi={false}
634
+ onChange={handleSelectedOcrKey}
635
+ options={ocrTemplateBuilder.selectedKey.map(
636
+ (a) => ({
637
+ label: a,
638
+ value: a,
639
+ })
640
+ )}
641
+ placeholder="Select a Key to use"
642
+ settings={{ id: id }}
643
+ style={{}}
644
+ isCreatable={false}
645
+ creatableConfig={{}}
646
+ />
647
+ <br />
648
+ <strong>File Name</strong>
649
+ <input
650
+ type="text"
651
+ value={data[id]?.dynamicFilename || ''}
652
+ data-id={id}
653
+ onChange={handleOcrInputChange}
654
+ name="dynamicFilename"
655
+ />
656
+ </>
657
+ )}
658
+ </>
659
+ );
660
+ };
661
+
440
662
  // Render based on the item's type
441
663
  switch (type) {
442
664
  case 'boolean':
@@ -461,6 +683,8 @@ function GenericDetail({
461
683
  return renderLatestNotes();
462
684
  case 'link':
463
685
  return renderLink();
686
+ case 'ocrTemplate':
687
+ return renderOcrTemplate(id);
464
688
  case 'options':
465
689
  return renderOptions();
466
690
  case 'password':
@@ -1064,6 +1288,7 @@ function GenericDetail({
1064
1288
 
1065
1289
  {activeTabConfig.form && (
1066
1290
  <div className={styles.polActions}>
1291
+ {/* Always render the "Edit" button if activeTabConfig.form exists */}
1067
1292
  <button
1068
1293
  className={styles.btn}
1069
1294
  onClick={() =>
@@ -1075,21 +1300,57 @@ function GenericDetail({
1075
1300
  >
1076
1301
  Edit
1077
1302
  </button>
1078
- </div>
1079
- )}
1080
1303
 
1081
- {activeTabConfig.export && (
1082
- <div className={styles.polActions}>
1083
- <button
1084
- className={styles.btn}
1085
- onClick={() =>
1086
- handleExport(
1087
- activeTabConfig.export
1088
- )
1089
- }
1090
- >
1091
- Export
1092
- </button>
1304
+ {/* Check if sections contain "ocrTemplate" type and render the "Save" button under the same polActions */}
1305
+ {activeTabConfig.sections &&
1306
+ activeTabConfig.sections
1307
+ .length > 0 &&
1308
+ activeTabConfig.sections.map(
1309
+ (section, sectionIndex) =>
1310
+ section.content &&
1311
+ section.content.some(
1312
+ (item) =>
1313
+ item.type ===
1314
+ 'ocrTemplate'
1315
+ ) && (
1316
+ <button
1317
+ key={`save-button-${sectionIndex}`}
1318
+ className={
1319
+ styles.btn
1320
+ }
1321
+ onClick={() => {
1322
+ // Handle the specific "ocrTemplate" item
1323
+ const ocrItem =
1324
+ section.content.find(
1325
+ (
1326
+ item
1327
+ ) =>
1328
+ item.type ===
1329
+ 'ocrTemplate'
1330
+ );
1331
+ handleOcrTemplateSave(
1332
+ ocrItem.id
1333
+ ); // Now you can safely use ocrItem.id
1334
+ }}
1335
+ >
1336
+ Save
1337
+ </button>
1338
+ )
1339
+ )}
1340
+
1341
+ {/* Check if export exists and render the "Export" button under the same polActions */}
1342
+ {activeTabConfig.export && (
1343
+ <button
1344
+ className={styles.btn}
1345
+ onClick={() =>
1346
+ handleExport(
1347
+ activeTabConfig.export
1348
+ )
1349
+ }
1350
+ >
1351
+ Export
1352
+ </button>
1353
+ )}
1093
1354
  </div>
1094
1355
  )}
1095
1356
  </>
@@ -1328,9 +1589,8 @@ function GenericDetail({
1328
1589
  schedulingConfig={activeTabConfig}
1329
1590
  data={data}
1330
1591
  dataId={routeParams[urlParam]}
1331
- handleReload={handleReload}
1592
+ modalOpen={modalOpen}
1332
1593
  preloadedOptions={preloadedOptions}
1333
- userProfile={userProfile}
1334
1594
  />
1335
1595
  );
1336
1596
  default:
@@ -16,9 +16,8 @@ const GenericEditableTable = ({
16
16
  schedulingConfig,
17
17
  data,
18
18
  dataId,
19
- handleReload,
19
+ modalOpen,
20
20
  preloadedOptions,
21
- userProfile,
22
21
  }) => {
23
22
  const { columns, rows, form } = schedulingConfig;
24
23
 
@@ -56,29 +55,37 @@ const GenericEditableTable = ({
56
55
 
57
56
  const groupCategoriesByType = (data) => {
58
57
  const grouped = {};
59
- data.forEach((category) => {
58
+
59
+ data.forEach((category, categoryKey) => {
60
60
  const categoryType =
61
- category[rows.categoryKey.id]?.[rows.categoryKey.label] ||
62
- 'Miscellaneous';
61
+ category[rows.categoryKey.id]?.[rows.categoryKey.label] || null;
62
+
63
+ const categoryId = category[rows.categoryKey.id]?.id || null;
63
64
 
64
- const categoryId =
65
- category[rows.categoryKey.id]?.id || Number.MAX_SAFE_INTEGER;
65
+ const groupKey = categoryId || 'no_category';
66
66
 
67
- if (!grouped[categoryId]) {
68
- grouped[categoryId] = {
69
- id: categoryId,
70
- category: categoryType,
67
+ if (!grouped[groupKey]) {
68
+ grouped[groupKey] = {
69
+ id: categoryId || 'no_category',
70
+ category: categoryType || '',
71
71
  entries: [],
72
72
  };
73
73
  }
74
- grouped[categoryId].entries.push(category);
74
+ grouped[groupKey].entries.push({
75
+ ...category,
76
+ keyCounter: categoryKey,
77
+ });
75
78
  });
76
79
 
77
80
  Object.values(grouped).forEach((group) => {
78
81
  group.entries.sort((a, b) => a.sort_order - b.sort_order);
79
82
  });
80
83
 
81
- return Object.values(grouped);
84
+ // Ensure 'no_category' group is at the top
85
+ const groupedArray = Object.values(grouped);
86
+ return groupedArray.sort((a, b) =>
87
+ a.id === 'no_category' ? -1 : b.id === 'no_category' ? 1 : 0
88
+ );
82
89
  };
83
90
 
84
91
  const handleFieldChange = (value, fieldId, keyCounter) => {
@@ -89,7 +96,7 @@ const GenericEditableTable = ({
89
96
  [fieldId]: value,
90
97
  };
91
98
 
92
- debouncedUpdate({ detail: updatedDetail });
99
+ debouncedUpdate({ [rows.key]: updatedDetail });
93
100
 
94
101
  return updatedDetail;
95
102
  });
@@ -130,7 +137,7 @@ const GenericEditableTable = ({
130
137
  : item;
131
138
  });
132
139
 
133
- debouncedUpdate({ detail: reordered });
140
+ debouncedUpdate({ [rows.key]: reordered });
134
141
  return reordered;
135
142
  }
136
143
  }
@@ -139,6 +146,30 @@ const GenericEditableTable = ({
139
146
  });
140
147
  };
141
148
 
149
+ const handleDeleteRow = (entry) => {
150
+ confirmAlert({
151
+ title: 'Confirm to Delete',
152
+ message: 'Are you sure you want to delete this entry?',
153
+ buttons: [
154
+ {
155
+ label: 'Yes',
156
+ onClick: async () => {
157
+ setLocalData((prev) => {
158
+ const updatedDetail = prev.filter(
159
+ (item) => item.id !== entry.id
160
+ );
161
+ debouncedUpdate({ [rows.key]: updatedDetail });
162
+ return updatedDetail;
163
+ });
164
+ },
165
+ },
166
+ {
167
+ label: 'No',
168
+ },
169
+ ],
170
+ });
171
+ };
172
+
142
173
  const renderScheduledTableField = (entry, column, keyCounter) => {
143
174
  if (!entry || !column) return null;
144
175
 
@@ -159,6 +190,21 @@ const GenericEditableTable = ({
159
190
  style={{ textAlign: 'right' }}
160
191
  />
161
192
  );
193
+ case 'date':
194
+ return (
195
+ <input
196
+ type="date"
197
+ value={entry[column.id] || ''}
198
+ onChange={(e) =>
199
+ handleFieldChange(
200
+ e.target.value,
201
+ column.id,
202
+ keyCounter
203
+ )
204
+ }
205
+ style={{ textAlign: 'right' }}
206
+ />
207
+ );
162
208
  case 'dropdown':
163
209
  return (
164
210
  <select
@@ -287,43 +333,49 @@ const GenericEditableTable = ({
287
333
  </td>
288
334
  ))}
289
335
  <td>
290
- {idx > 0 && (
291
- <ArrowUp
292
- size={18}
293
- onClick={() =>
294
- handleSortChange(
295
- group.id,
296
- entry.id,
297
- 'up'
298
- )
299
- }
300
- style={{
301
- cursor: 'pointer',
302
- }}
303
- />
304
- )}
305
- {idx < group.entries.length - 1 && (
306
- <ArrowDown
336
+ <div className={styles.tdactions}>
337
+ {idx > 0 && (
338
+ <ArrowUp
339
+ size={18}
340
+ onClick={() =>
341
+ handleSortChange(
342
+ group.id,
343
+ entry.id,
344
+ 'up'
345
+ )
346
+ }
347
+ style={{
348
+ cursor: 'pointer',
349
+ }}
350
+ />
351
+ )}
352
+ {idx <
353
+ group.entries.length -
354
+ 1 && (
355
+ <ArrowDown
356
+ size={18}
357
+ onClick={() =>
358
+ handleSortChange(
359
+ group.id,
360
+ entry.id,
361
+ 'down'
362
+ )
363
+ }
364
+ style={{
365
+ cursor: 'pointer',
366
+ }}
367
+ />
368
+ )}
369
+ <TrashCan
307
370
  size={18}
308
371
  onClick={() =>
309
- handleSortChange(
310
- group.id,
311
- entry.id,
312
- 'down'
313
- )
372
+ handleDeleteRow(entry)
314
373
  }
315
374
  style={{
316
375
  cursor: 'pointer',
317
376
  }}
318
377
  />
319
- )}
320
- <TrashCan
321
- size={18}
322
- onClick={() =>
323
- handleDeleteRow(entry)
324
- }
325
- style={{ cursor: 'pointer' }}
326
- />
378
+ </div>
327
379
  </td>
328
380
  </tr>
329
381
  ))}
@@ -332,8 +384,22 @@ const GenericEditableTable = ({
332
384
  </tbody>
333
385
  </table>
334
386
  ) : (
335
- <div>No data available.</div>
387
+ <div className={styles.noDataMessage}>
388
+ No data available. Please check your filters or try
389
+ reloading the data.
390
+ </div>
336
391
  )}
392
+
393
+ <div className={styles.polActions}>
394
+ <button
395
+ className={styles.btn}
396
+ onClick={() => {
397
+ modalOpen('create', 0);
398
+ }}
399
+ >
400
+ Add
401
+ </button>
402
+ </div>
337
403
  </div>
338
404
  );
339
405
  };
@@ -662,6 +662,33 @@
662
662
  text-align: right;
663
663
  }
664
664
 
665
+ .schedulingTable td input[type='checkbox'] {
666
+ width: 20px !important;
667
+ height: 20px;
668
+ border-radius: 4px;
669
+ background-color: var(--item-color);
670
+ border: 2px solid rgba(var(--primary-rgb), 0.15);
671
+ position: relative;
672
+ cursor: pointer;
673
+ transition: background-color 0.3s, border-color 0.3s;
674
+ }
675
+
676
+ .schedulingTable td input[type='checkbox']:checked {
677
+ background-color: var(--primary-color);
678
+ border-color: var(--highlight-color);
679
+ }
680
+
681
+ .schedulingTable td input[type='checkbox']:focus {
682
+ outline: none;
683
+ box-shadow: 0 0 0 2px rgba(var(--primary-rgb), 0.2);
684
+ }
685
+
686
+ /* Custom hover effect for better user experience */
687
+ .schedulingTable td input[type='checkbox']:hover {
688
+ background-color: rgba(var(--primary-rgb), 0.1);
689
+ border-color: rgba(var(--primary-rgb), 0.25);
690
+ }
691
+
665
692
  .noDataMessage {
666
693
  text-align: center;
667
694
  color: var(--secondary-color); /* Maintain a clear and neutral color */
@@ -134,3 +134,45 @@
134
134
  background: var(--highlight-color);
135
135
  border: 1px solid rgba(var(--highlight-rgb), 1.05);
136
136
  }
137
+
138
+ .tdactions {
139
+ width: 100%;
140
+ display: flex;
141
+ flex-wrap: nowrap;
142
+ justify-content: flex-start;
143
+ align-items: center;
144
+ gap: 0.1rem;
145
+
146
+ span {
147
+ display: flex;
148
+ align-items: center;
149
+ position: relative;
150
+ width: 18px;
151
+ height: 18px;
152
+
153
+ svg {
154
+ width: 18px;
155
+ height: 18px;
156
+ }
157
+ }
158
+ }
159
+
160
+ .noDataMessage {
161
+ text-align: center;
162
+ color: var(--primary-color);
163
+ font-size: 1rem;
164
+ padding: 2rem;
165
+ margin: 2rem auto;
166
+ background-color: var(--secondary-bg-color, #f8f8f8);
167
+ border: 1px solid rgba(var(--primary-rgb), 0.15);
168
+ border-radius: 6px;
169
+ max-width: 80%;
170
+ box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.1);
171
+ font-weight: 500;
172
+ font-family: var(--font-family, Arial, sans-serif);
173
+ }
174
+
175
+ .noDataMessage:hover {
176
+ background-color: rgba(var(--primary-rgb), 0.05);
177
+ border-color: rgba(var(--primary-rgb), 0.2);
178
+ }