@visns-studio/visns-components 4.7.2 → 4.7.3

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 (99) hide show
  1. package/package.json +13 -9
  2. package/src/components/cms/DataGrid.jsx +1461 -0
  3. package/src/components/cms/DropZone.jsx +349 -0
  4. package/src/components/cms/Field.jsx +802 -0
  5. package/src/components/cms/Form.jsx +476 -0
  6. package/src/components/cms/generic/CmsDetail.jsx +36 -0
  7. package/src/components/cms/generic/CmsIndex.jsx +73 -0
  8. package/src/components/cms/generic/CmsSort.jsx +125 -0
  9. package/src/components/cms/sorting/Item.jsx +115 -0
  10. package/src/components/cms/sorting/List.jsx +30 -0
  11. package/src/components/crm/AsyncSelect.jsx +125 -0
  12. package/src/components/crm/Autocomplete.jsx +174 -0
  13. package/src/components/crm/Breadcrumb.jsx +52 -0
  14. package/src/components/crm/Call.jsx +220 -0
  15. package/src/components/crm/DataGrid.jsx +2701 -0
  16. package/src/components/crm/Download.jsx +101 -0
  17. package/src/components/crm/Fetch.jsx +119 -0
  18. package/src/components/crm/Field.jsx +1557 -0
  19. package/src/components/crm/Form.jsx +1409 -0
  20. package/src/components/crm/Loader.jsx +29 -0
  21. package/src/components/crm/MultiSelect.jsx +78 -0
  22. package/src/components/crm/Navigation.jsx +619 -0
  23. package/src/components/crm/Notification.jsx +197 -0
  24. package/src/components/crm/QrCode.jsx +83 -0
  25. package/src/components/crm/Select.jsx +58 -0
  26. package/src/components/crm/SelectList.jsx +21 -0
  27. package/src/components/crm/TableFilter.jsx +198 -0
  28. package/src/components/crm/auth/Login.jsx +223 -0
  29. package/src/components/crm/auth/Profile.jsx +280 -0
  30. package/src/components/crm/auth/Reset.jsx +111 -0
  31. package/src/components/crm/auth/Verify.jsx +174 -0
  32. package/src/components/crm/auth/styles/Login.module.scss +243 -0
  33. package/src/components/crm/auth/styles/Profile.module.scss +222 -0
  34. package/src/components/crm/auth/styles/Reset.module.scss +216 -0
  35. package/src/components/crm/auth/styles/Verify.module.scss +216 -0
  36. package/src/components/crm/generic/AuditLog.jsx +108 -0
  37. package/src/components/crm/generic/AuditLogs.jsx +115 -0
  38. package/src/components/crm/generic/GenericAuth.jsx +166 -0
  39. package/src/components/crm/generic/GenericDashboard.jsx +42 -0
  40. package/src/components/crm/generic/GenericDetail.jsx +1499 -0
  41. package/src/components/crm/generic/GenericDynamic.jsx +915 -0
  42. package/src/components/crm/generic/GenericFormBuilder.jsx +1765 -0
  43. package/src/components/crm/generic/GenericIndex.jsx +376 -0
  44. package/src/components/crm/generic/GenericMain.jsx +129 -0
  45. package/src/components/crm/generic/GenericSort.jsx +188 -0
  46. package/src/components/crm/generic/NotificationList.jsx +64 -0
  47. package/src/components/crm/generic/styles/AuditLog.module.scss +147 -0
  48. package/src/components/crm/generic/styles/AuditLogs.module.scss +92 -0
  49. package/src/components/crm/generic/styles/GenericDetail.module.scss +634 -0
  50. package/src/components/crm/generic/styles/GenericDynamic.module.scss +932 -0
  51. package/src/components/crm/generic/styles/GenericFormBuilder.module.scss +944 -0
  52. package/src/components/crm/generic/styles/GenericIndex.module.scss +247 -0
  53. package/src/components/crm/generic/styles/GenericMain.module.scss +91 -0
  54. package/src/components/crm/generic/styles/GenericSort.module.scss +123 -0
  55. package/src/components/crm/generic/styles/NotificationList.module.scss +67 -0
  56. package/src/components/crm/sorting/Item.jsx +117 -0
  57. package/src/components/crm/sorting/List.jsx +32 -0
  58. package/src/components/crm/sorting/styles/Item.module.scss +68 -0
  59. package/src/components/crm/sorting/styles/List.module.scss +68 -0
  60. package/src/components/crm/styles/AsyncSelect.module.scss +8 -0
  61. package/src/components/crm/styles/Autocomplete.module.scss +44 -0
  62. package/src/components/crm/styles/DataGrid.module.scss +210 -0
  63. package/src/components/crm/styles/Field.module.scss +311 -0
  64. package/src/components/crm/styles/Form.module.scss +499 -0
  65. package/src/components/crm/styles/Loader.module.scss +94 -0
  66. package/src/components/crm/styles/MultiSelect.module.scss +8 -0
  67. package/src/components/crm/styles/Navigation.module.scss +978 -0
  68. package/src/components/crm/styles/Notification.module.scss +147 -0
  69. package/src/components/crm/styles/QrCode.module.scss +87 -0
  70. package/src/components/crm/styles/TableFilter.module.scss +52 -0
  71. package/src/components/styles/global.css +73 -0
  72. package/src/index.js +86 -0
  73. package/dist/104e5636d8ddb791614c.woff2 +0 -0
  74. package/dist/11fe6a83d647de5daf95.woff +0 -0
  75. package/dist/141e2a8c7836495afe2a.woff +0 -0
  76. package/dist/2ba5877f7079830b5383.woff +0 -0
  77. package/dist/2d6836f45b2037be41f3.woff +0 -0
  78. package/dist/399.0a0f3e0d21be1057d599.css +0 -8
  79. package/dist/399.b8f57fa3f8c0c10119e4.js +0 -1
  80. package/dist/47d566c3b1293dfd9d77.woff +0 -0
  81. package/dist/6282a13cbba671a71913.woff2 +0 -0
  82. package/dist/78266f551031aa9db127.woff +0 -0
  83. package/dist/7b7da89f1e3fdcbfe36c.woff2 +0 -0
  84. package/dist/7e78e8338af7efeda712.woff2 +0 -0
  85. package/dist/7fa387951673abf164b1.woff2 +0 -0
  86. package/dist/819d59862c2d0640aad8.woff +0 -0
  87. package/dist/b615e1a6c838ea059a56.woff +0 -0
  88. package/dist/b9b1aa739c660c4ce67c.woff2 +0 -0
  89. package/dist/c5f3640b11df9414facb.woff +0 -0
  90. package/dist/da26d162ff5af71d6590.woff2 +0 -0
  91. package/dist/dd5b2912dbf896310865.woff2 +0 -0
  92. package/dist/f282b331272463c1aa6c.woff2 +0 -0
  93. package/dist/index.css +0 -2902
  94. package/dist/index.js +0 -2
  95. package/dist/index.js.LICENSE.txt +0 -150
  96. package/dist/main.83b033670d64419bb497.css +0 -1
  97. package/dist/main.b1723d17cd2d3513ab62.js +0 -1
  98. package/dist/report.html +0 -39
  99. package/dist/runtime.33c276593b23f8035b8d.js +0 -1
@@ -0,0 +1,1499 @@
1
+ import '../../styles/global.css';
2
+
3
+ import React, { useCallback, useEffect, useRef, useState } from 'react';
4
+ import { Link, useParams } from 'react-router-dom';
5
+ import moment from 'moment';
6
+ import parse from 'html-react-parser';
7
+ import Popup from 'reactjs-popup';
8
+ import { motion } from 'framer-motion';
9
+ import Dropzone from 'react-dropzone';
10
+ import { confirmAlert } from 'react-confirm-alert';
11
+ import truncate from 'truncate';
12
+ import { CopyToClipboard } from 'react-copy-to-clipboard';
13
+ import { toast } from 'react-toastify';
14
+ import {
15
+ CircleCheck,
16
+ Copy,
17
+ EyeOpen,
18
+ EyeSlashed,
19
+ File,
20
+ TrashCan,
21
+ } from 'akar-icons';
22
+
23
+ import 'react-confirm-alert/src/react-confirm-alert.css';
24
+
25
+ import Breadcrumb from '../Breadcrumb';
26
+ import CustomFetch from '../Fetch';
27
+ import Form from '../Form';
28
+ import GenericDynamic from './GenericDynamic';
29
+ import QrCode from '../QrCode';
30
+ import Table from '../DataGrid';
31
+ import TableFilter from '../TableFilter';
32
+
33
+ import styles from './styles/GenericDetail.module.scss';
34
+
35
+ function GenericDetail({
36
+ extraUrlParam,
37
+ layout = 'full',
38
+ setting,
39
+ urlParam,
40
+ userProfile,
41
+ }) {
42
+ const gridRef = useRef(null);
43
+ const currentConfigRef = useRef(null);
44
+ const routeParams = useParams();
45
+
46
+ const { filters, page, stages, tabs } = setting;
47
+
48
+ /** Fileupload states */
49
+ const [files, setFiles] = useState([]);
50
+ const [loadingProgress, setLoadingProgress] = useState(0);
51
+
52
+ /** General States */
53
+ const [activeStage, setActiveStage] = useState('');
54
+ const [config, setConfig] = useState({});
55
+ const [data, setData] = useState({});
56
+ const [dataReload, setDataReload] = useState(0);
57
+ const [rowsSelected, setRowsSelected] = useState({});
58
+ const [subnav, setSubnav] = useState([]);
59
+ const [total, setTotal] = useState(0);
60
+ const [windowHeight, setWindowHeight] = useState(window.innerHeight);
61
+
62
+ const [passwordVisible, setPasswordVisible] = useState(false);
63
+
64
+ const togglePasswordVisibility = () => {
65
+ setPasswordVisible(!passwordVisible);
66
+ };
67
+
68
+ /** General Functions */
69
+ const handleStageUpdate = async (stage, type) => {
70
+ try {
71
+ const activeTab = subnav.find((s) => s.show === true);
72
+
73
+ if (activeTab) {
74
+ const activeTabConfig = tabs.find((t) => t.id === activeTab.id);
75
+
76
+ if (type === 'view') {
77
+ setActiveStage(stage.id);
78
+ } else {
79
+ if (
80
+ activeTabConfig.stages &&
81
+ activeTabConfig.stages.url &&
82
+ activeTabConfig.stages.url !== '' &&
83
+ activeTabConfig.stages.key &&
84
+ activeTabConfig.stages.key !== '' &&
85
+ activeTabConfig.stages.urlParam &&
86
+ activeTabConfig.stages.urlParam !== ''
87
+ ) {
88
+ const url = `${activeTabConfig.stages.url}/${routeParams[urlParam]}`;
89
+
90
+ const res = await CustomFetch(url, 'POST', {
91
+ [activeTabConfig.stages.key]: stage.id,
92
+ });
93
+
94
+ if (res.data.error === '') {
95
+ handleReload();
96
+ } else {
97
+ toast.error(res.data.error);
98
+ }
99
+ }
100
+ }
101
+ }
102
+ } catch (error) {
103
+ console.error(error);
104
+ }
105
+ };
106
+
107
+ const handleCheckboxUpdate = async () => {
108
+ try {
109
+ if (currentConfigRef.current.functions.checkboxUpdate) {
110
+ const { data, method, message, url } =
111
+ currentConfigRef.current.functions.checkboxUpdate;
112
+ if (!Object.keys(rowsSelected).length) {
113
+ toast.warning(message.warning);
114
+ return;
115
+ }
116
+ const res = await CustomFetch(url, method, {
117
+ ...data,
118
+ rows: rowsSelected,
119
+ });
120
+ if (res.data.error === '') {
121
+ toast.success(message.success);
122
+ gridRef.current.reload();
123
+ } else {
124
+ toast.error(message.error);
125
+ }
126
+ }
127
+ } catch (error) {
128
+ console.error(error);
129
+ }
130
+ };
131
+
132
+ const handleReload = () => {
133
+ const min = 0;
134
+ const max = 999999999;
135
+ const randomNumber = Math.floor(Math.random() * (max - min + 1) + min);
136
+
137
+ setDataReload(randomNumber);
138
+ };
139
+
140
+ const getValueFromData = (data, path, id, type) => {
141
+ let value =
142
+ path.reduce((result, key) => (result ? result[key] : ''), data) ??
143
+ '';
144
+
145
+ if (value) {
146
+ if (Array.isArray(value)) {
147
+ return value
148
+ .filter((item) => item[id]) // Filter out items without the specified id
149
+ .map((item) => item[id]) // Extract the id from each item
150
+ .join(', ');
151
+ } else {
152
+ if (Array.isArray(id)) {
153
+ if (type === 'checkbox') {
154
+ return value[id];
155
+ } else {
156
+ return id.map((key) => value[key]).join(' ');
157
+ }
158
+ } else {
159
+ return value[id];
160
+ }
161
+ }
162
+ } else {
163
+ if (path.length > 0) {
164
+ return '';
165
+ } else {
166
+ return data[id] || '';
167
+ }
168
+ }
169
+ };
170
+
171
+ const formatDate = (date) => {
172
+ return date && moment(date).format('DD-MM-YYYY') !== '01-01-1970'
173
+ ? moment(date).format('DD-MM-YYYY')
174
+ : '';
175
+ };
176
+
177
+ const formatDateTime = (date) => {
178
+ return date && moment(date).format('DD-MM-YYYY') !== '01-01-1970'
179
+ ? moment(date).format('DD-MM-YYYY HH:mm A')
180
+ : '';
181
+ };
182
+
183
+ const renderValue = (item, data) => {
184
+ const {
185
+ type,
186
+ id,
187
+ options,
188
+ relation,
189
+ relationKey,
190
+ size,
191
+ truncateLength,
192
+ } = item;
193
+ const value =
194
+ relation && relation.length > 0 && type !== 'total'
195
+ ? getValueFromData(data, [...relation], id, type)
196
+ : getValueFromData(data, [], id, type);
197
+
198
+ const stringValue = String(value ?? ''); // Convert value to a string
199
+
200
+ // Conditionally truncate the value if the 'truncate' key is present
201
+ const truncatedValue =
202
+ truncateLength && truncateLength > 0
203
+ ? truncate(stringValue, truncateLength)
204
+ : stringValue;
205
+
206
+ const formatDateValue = (date) => formatDate(date);
207
+ const formatDateTimeValue = (date) => formatDateTime(date);
208
+
209
+ const formatNoteValue = () => {
210
+ const noteData = data?.[id]?.[0];
211
+ const name = noteData?.user?.name ?? '';
212
+ const description = noteData?.description ?? '';
213
+ const created_at = noteData?.created_at ?? '';
214
+ const formattedDate = formatDateValue(created_at);
215
+
216
+ return name !== '' && description !== '' && formattedDate !== ''
217
+ ? parse(
218
+ `<br /><p>${name} - ${formattedDate}</p><p>${description}</p>`
219
+ )
220
+ : null;
221
+ };
222
+
223
+ const formatTotalValue = () => {
224
+ const total =
225
+ data[relation]?.reduce(
226
+ (acc, r) => acc + (parseFloat(r[relationKey]) || 0),
227
+ 0
228
+ ) || 0;
229
+ return `${total} / ${value}`;
230
+ };
231
+
232
+ const renderCopyToClipboard = () => {
233
+ return (
234
+ <>
235
+ {size === 'full' && truncatedValue
236
+ ? parse(`<br /><p>${truncatedValue}</p>`)
237
+ : truncatedValue}
238
+ aaa
239
+ <CopyToClipboard
240
+ text={
241
+ truncatedValue && truncatedValue !== 'null'
242
+ ? truncatedValue
243
+ : ''
244
+ }
245
+ onCopy={() => console.info('copied')}
246
+ >
247
+ <Copy
248
+ data-tooltip-id="system-tooltip"
249
+ data-tooltip-content="Copy to Clipboard"
250
+ strokeWidth={2}
251
+ size={24}
252
+ />
253
+ </CopyToClipboard>
254
+ </>
255
+ );
256
+ };
257
+
258
+ const renderBoolean = () => (value ? 'Yes' : 'No');
259
+
260
+ const renderCheckbox = () => {
261
+ if (!value || typeof value !== 'object') {
262
+ return '';
263
+ }
264
+
265
+ const formatKey = (key) => {
266
+ return key
267
+ .toLowerCase()
268
+ .split('_')
269
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
270
+ .join(' ');
271
+ };
272
+
273
+ const keys = Object.keys(value)
274
+ .filter((key) => value[key] && key !== 'notes')
275
+ .sort((a, b) => {
276
+ if (a === 'other') return 1;
277
+ if (b === 'other') return -1;
278
+ return a.localeCompare(b);
279
+ });
280
+
281
+ const formattedKeys = keys.map((key) => {
282
+ let formattedKey = formatKey(key);
283
+ if (key === 'other') {
284
+ const otherValue = getValueFromData(
285
+ data,
286
+ [...relation],
287
+ `${id}_other`,
288
+ type
289
+ );
290
+
291
+ if (otherValue) {
292
+ formattedKey += `: ${otherValue}`;
293
+ }
294
+ }
295
+ return formattedKey;
296
+ });
297
+
298
+ return formattedKeys.join(', ');
299
+ };
300
+
301
+ const renderDate = () => formatDateValue(value);
302
+
303
+ const renderDateTime = () => formatDateTimeValue(value);
304
+
305
+ const renderFile = () =>
306
+ value ? (
307
+ <a href={value} target="_blank">
308
+ Download
309
+ </a>
310
+ ) : null;
311
+
312
+ const renderExist = () => (truncatedValue ? 'Yes' : 'No');
313
+
314
+ const renderJson = () =>
315
+ value ? <pre>{JSON.stringify(value, null, 4)}</pre> : null;
316
+
317
+ const renderLatestNotes = () => formatNoteValue();
318
+
319
+ const renderOptions = () => (options ? options[value] : '');
320
+
321
+ const renderRichText = () => {
322
+ if (!truncatedValue) {
323
+ return null;
324
+ }
325
+
326
+ // Split the string on both '\n' and '<br>' using a regular expression.
327
+ const lines = truncatedValue.split(/<br>|\\n/);
328
+
329
+ if (lines.length === 0) {
330
+ return null;
331
+ }
332
+
333
+ return (
334
+ <>
335
+ {lines.map((line, index) => (
336
+ <p key={index}>{line}</p>
337
+ ))}
338
+ </>
339
+ );
340
+ };
341
+
342
+ const renderTotal = () => formatTotalValue();
343
+
344
+ const renderPassword = () => {
345
+ return (
346
+ <>
347
+ {passwordVisible ? stringValue : '******'}
348
+ <button
349
+ type="button"
350
+ onClick={togglePasswordVisibility}
351
+ style={{
352
+ marginLeft: '10px',
353
+ background: 'none',
354
+ border: 'none',
355
+ cursor: 'pointer',
356
+ }}
357
+ >
358
+ {passwordVisible ? (
359
+ <EyeSlashed size={24} />
360
+ ) : (
361
+ <EyeOpen size={24} />
362
+ )}
363
+ </button>
364
+ </>
365
+ );
366
+ };
367
+
368
+ // Render based on the item's type
369
+ switch (type) {
370
+ case 'boolean':
371
+ return renderBoolean();
372
+ case 'checkbox':
373
+ return renderCheckbox();
374
+ case 'copyToClipboard':
375
+ return renderCopyToClipboard();
376
+ case 'date':
377
+ return renderDate();
378
+ case 'datetime':
379
+ return renderDateTime();
380
+ case 'exist':
381
+ return renderExist();
382
+ case 'file':
383
+ return renderFile();
384
+ case 'json':
385
+ return renderJson();
386
+ case 'latest_notes':
387
+ return renderLatestNotes();
388
+ case 'options':
389
+ return renderOptions();
390
+ case 'richtext':
391
+ return renderRichText();
392
+ case 'total':
393
+ return renderTotal();
394
+ case 'password':
395
+ return renderPassword();
396
+ default:
397
+ return size === 'full' && truncatedValue
398
+ ? parse(`<br /><p>${truncatedValue}</p>`)
399
+ : truncatedValue;
400
+ }
401
+ };
402
+
403
+ const renderContent = () => {
404
+ if (subnav && subnav.length > 0) {
405
+ const activeTab = subnav.find((s) => s.show === true);
406
+
407
+ if (activeTab) {
408
+ const activeTabConfig = tabs.find((t) => t.id === activeTab.id);
409
+ currentConfigRef.current = activeTabConfig;
410
+
411
+ if (activeTabConfig && activeTabConfig.type) {
412
+ switch (activeTabConfig.type) {
413
+ case 'dropzone':
414
+ const downloadFile = (id) => {
415
+ window.open(
416
+ `/ajax/files/download/${id}/${activeTabConfig.folder}`
417
+ );
418
+ };
419
+
420
+ const deleteFile = (e, id, name) => {
421
+ if (e) {
422
+ e.stopPropagation();
423
+
424
+ confirmAlert({
425
+ title: 'Confirm to Delete File',
426
+ message:
427
+ 'Are you sure you want to delete ' +
428
+ name,
429
+ buttons: [
430
+ {
431
+ label: 'Yes',
432
+ onClick: () => {
433
+ CustomFetch(
434
+ `${activeTabConfig.deleteUrl}`,
435
+ 'POST',
436
+ {
437
+ file_id: id,
438
+ },
439
+ function (result) {
440
+ if (
441
+ result.error ===
442
+ ''
443
+ ) {
444
+ handleReload();
445
+ } else {
446
+ toast.error(
447
+ result.error
448
+ );
449
+ }
450
+ }
451
+ );
452
+ },
453
+ },
454
+ {
455
+ label: 'No',
456
+ onClick: () => close(),
457
+ },
458
+ ],
459
+ });
460
+ }
461
+ };
462
+
463
+ return (
464
+ <div className={styles.gridtxt}>
465
+ <div className={styles.gridtxt__header}>
466
+ {activeTabConfig.label
467
+ ? activeTabConfig.label
468
+ : 'Files'}
469
+ </div>
470
+ <div className={styles.progress}>
471
+ <motion.div
472
+ className={styles.progress__bar}
473
+ initial={{ width: '0%' }}
474
+ animate={{
475
+ width: loadingProgress + '%',
476
+ }}
477
+ transition={{
478
+ duration: 2,
479
+ ease: [0.165, 0.84, 0.44, 1.0],
480
+ }}
481
+ />
482
+ </div>
483
+ <Dropzone
484
+ onDrop={(acceptedFiles) => {
485
+ const uploadFile = (file) => {
486
+ Vapor.store(file, {
487
+ progress: (progress) => {
488
+ setLoadingProgress(
489
+ progress * 100
490
+ );
491
+ },
492
+ }).then((response) => {
493
+ setLoadingProgress(0);
494
+ const requestBody = {
495
+ uuid: response.uuid,
496
+ key: response.key,
497
+ bucket: response.bucket,
498
+ filename: file.name,
499
+ filesize: file.size,
500
+ extension:
501
+ response.extension,
502
+ file_relationship:
503
+ activeTabConfig.file_relationship,
504
+ };
505
+
506
+ CustomFetch(
507
+ `${activeTabConfig.url}/${routeParams[urlParam]}`,
508
+ 'PUT',
509
+ requestBody,
510
+ (res) => {
511
+ setFiles(
512
+ res.data.files
513
+ );
514
+ }
515
+ );
516
+ });
517
+ };
518
+
519
+ acceptedFiles.forEach((a) => {
520
+ uploadFile(a);
521
+ });
522
+ }}
523
+ >
524
+ {({
525
+ getRootProps,
526
+ getInputProps,
527
+ isDragActive,
528
+ }) => (
529
+ <section
530
+ className={
531
+ styles.dropzone__container
532
+ }
533
+ >
534
+ <div {...getRootProps()}>
535
+ <input
536
+ {...getInputProps()}
537
+ />
538
+ {isDragActive ? (
539
+ <p>
540
+ Drop the files here
541
+ ...
542
+ </p>
543
+ ) : (
544
+ <p>
545
+ Drag 'n' drop some
546
+ files here, or click
547
+ to select files
548
+ </p>
549
+ )}
550
+ </div>
551
+ {activeTabConfig.filetype ===
552
+ 'image' ? (
553
+ <div>
554
+ <ul
555
+ className={
556
+ styles.sortlist
557
+ }
558
+ >
559
+ {files.map(
560
+ (a, b) =>
561
+ a.base64_encoded_image && (
562
+ <li
563
+ key={`tf-${b}`}
564
+ >
565
+ {
566
+ a.file_name
567
+ }
568
+ <div
569
+ className={
570
+ styles.sortimg
571
+ }
572
+ >
573
+ <img
574
+ src={
575
+ a.base64_encoded_image
576
+ }
577
+ width="150"
578
+ />
579
+ </div>
580
+ <TrashCan
581
+ onClick={(
582
+ event
583
+ ) => {
584
+ deleteFile(
585
+ event,
586
+ a.id,
587
+ a.file_name
588
+ );
589
+ }}
590
+ strokeWidth={
591
+ 2
592
+ }
593
+ size={
594
+ 18
595
+ }
596
+ className="delete trash-delete"
597
+ />
598
+ </li>
599
+ )
600
+ )}
601
+ </ul>
602
+ </div>
603
+ ) : (
604
+ <ul
605
+ className={
606
+ styles.dropzone__files
607
+ }
608
+ >
609
+ {files.map((a, b) => (
610
+ <li
611
+ onClick={() => {
612
+ downloadFile(
613
+ a.id
614
+ );
615
+ }}
616
+ key={'tf-' + b}
617
+ >
618
+ <File
619
+ strokeWidth={
620
+ 2
621
+ }
622
+ size={18}
623
+ />
624
+ {a.file_name}
625
+ <button
626
+ onClick={(
627
+ event
628
+ ) => {
629
+ deleteFile(
630
+ event,
631
+ a.id,
632
+ a.file_name
633
+ );
634
+ }}
635
+ >
636
+ <TrashCan
637
+ strokeWidth={
638
+ 2
639
+ }
640
+ size={
641
+ 18
642
+ }
643
+ />
644
+ </button>
645
+ </li>
646
+ ))}
647
+ </ul>
648
+ )}
649
+ </section>
650
+ )}
651
+ </Dropzone>
652
+ </div>
653
+ );
654
+ case 'dynamicform':
655
+ return (
656
+ <GenericDynamic
657
+ buttons={
658
+ activeTabConfig.setting?.buttons
659
+ ? activeTabConfig.setting.buttons
660
+ : []
661
+ }
662
+ data={data}
663
+ fetchData={handleReload}
664
+ label={
665
+ data.task_template
666
+ ? data.task_template.label
667
+ : ''
668
+ }
669
+ multiple={false}
670
+ originalData={data}
671
+ primaryKey={activeTabConfig.setting.key}
672
+ setData={setData}
673
+ setting={activeTabConfig.setting.url}
674
+ type={activeTabConfig.setting.type}
675
+ />
676
+ );
677
+ case 'form':
678
+ const updateValueFromParam = (item, value) => {
679
+ if (item && item.urlParam) {
680
+ item.value = value;
681
+ }
682
+ };
683
+
684
+ const updateFieldValues = (
685
+ formConfig,
686
+ routeParams
687
+ ) => {
688
+ if (formConfig.stages) {
689
+ formConfig.stages.data.forEach((stage) => {
690
+ const stageFormFields =
691
+ stage.form?.fields || [];
692
+ const stageAjaxWhereFields = [];
693
+
694
+ stageFormFields.forEach((field) => {
695
+ if (field.type === 'table') {
696
+ if (
697
+ field.ajaxSetting &&
698
+ field.ajaxSetting.where
699
+ ) {
700
+ stageAjaxWhereFields.push(
701
+ ...field.ajaxSetting
702
+ .where
703
+ );
704
+ }
705
+
706
+ const tableFormFields =
707
+ field.form?.fields || [];
708
+ tableFormFields.forEach(
709
+ (formField) => {
710
+ updateValueFromParam(
711
+ formField,
712
+ routeParams[
713
+ formField
714
+ .urlParam
715
+ ]
716
+ );
717
+ }
718
+ );
719
+ }
720
+ });
721
+
722
+ stageAjaxWhereFields.forEach(
723
+ (where) => {
724
+ updateValueFromParam(
725
+ where,
726
+ routeParams[where.urlParam]
727
+ );
728
+ }
729
+ );
730
+ });
731
+ }
732
+ };
733
+
734
+ const formConfig = activeTabConfig;
735
+ updateFieldValues(formConfig, routeParams);
736
+
737
+ return (
738
+ <>
739
+ {formConfig.stages?.data?.length > 0 && (
740
+ <div className={styles.grid}>
741
+ <div className={styles.grid__row}>
742
+ <div
743
+ className={
744
+ styles.grid__full
745
+ }
746
+ >
747
+ {formConfig.stages.data.map(
748
+ (stage) =>
749
+ parseInt(
750
+ activeStage
751
+ ) === stage.id && (
752
+ <React.Fragment
753
+ key={`stage-form-${stage.id}`}
754
+ >
755
+ {stage.form && (
756
+ <div
757
+ className={
758
+ styles.gridtxt
759
+ }
760
+ >
761
+ {stage
762
+ .form
763
+ .update
764
+ .title && (
765
+ <div
766
+ className={
767
+ styles.gridtxt__header
768
+ }
769
+ >
770
+ <span>
771
+ {
772
+ stage
773
+ .form
774
+ .update
775
+ .title
776
+ }
777
+ </span>
778
+ </div>
779
+ )}
780
+ <Form
781
+ columnId={
782
+ routeParams[
783
+ urlParam
784
+ ]
785
+ }
786
+ formSettings={
787
+ stage.form
788
+ }
789
+ formType="update"
790
+ paramValue={
791
+ routeParams[
792
+ urlParam
793
+ ]
794
+ ? routeParams[
795
+ urlParam
796
+ ]
797
+ : null
798
+ }
799
+ style={
800
+ userProfile
801
+ ?.settings
802
+ ?.style ||
803
+ {}
804
+ }
805
+ type="inline"
806
+ updateForm={
807
+ setFormData
808
+ }
809
+ userProfile={
810
+ userProfile
811
+ }
812
+ />
813
+ </div>
814
+ )}
815
+ </React.Fragment>
816
+ )
817
+ )}
818
+ </div>
819
+ </div>
820
+ </div>
821
+ )}
822
+
823
+ {formConfig.form && (
824
+ <div className={styles.gridtxt}>
825
+ <div
826
+ className={
827
+ styles.gridtxt__header
828
+ }
829
+ >
830
+ <span>{formConfig.title}</span>
831
+ </div>
832
+ <Form
833
+ columnId={routeParams[urlParam]}
834
+ formSettings={formConfig.form}
835
+ formType="update"
836
+ paramValue={
837
+ routeParams[urlParam]
838
+ ? routeParams[urlParam]
839
+ : null
840
+ }
841
+ style={
842
+ userProfile?.settings
843
+ ?.style || {}
844
+ }
845
+ updateForm={setFormData}
846
+ type="inline"
847
+ userProfile={userProfile}
848
+ />
849
+ </div>
850
+ )}
851
+ </>
852
+ );
853
+ case 'overview':
854
+ return (
855
+ <>
856
+ <div className={styles.gridtxt}>
857
+ {activeTabConfig.sections.map(
858
+ (section, sectionIndex) => (
859
+ <React.Fragment
860
+ key={`section-${activeTabConfig.id}-${sectionIndex}`}
861
+ >
862
+ <div
863
+ className={
864
+ styles.gridtxt__header
865
+ }
866
+ >
867
+ <span>
868
+ {section.title}
869
+ </span>
870
+ </div>
871
+ {section.content &&
872
+ section.content.length >
873
+ 0 && (
874
+ <ul
875
+ className={
876
+ styles.customer__overview
877
+ }
878
+ >
879
+ {section.content.map(
880
+ (
881
+ item,
882
+ itemKey
883
+ ) => (
884
+ <li
885
+ key={`li-${item.id}-${itemKey}`}
886
+ className={
887
+ item.size ===
888
+ 'full'
889
+ ? `${styles['fw-grid-item']} ${styles.notecolor}`
890
+ : null
891
+ }
892
+ >
893
+ <strong>
894
+ {item.label
895
+ ? `${item.label}:`
896
+ : null}
897
+ </strong>{' '}
898
+ {item
899
+ .href
900
+ ?.url &&
901
+ item
902
+ .href
903
+ ?.key ? (
904
+ <Link
905
+ to={`${
906
+ item
907
+ .href
908
+ .url
909
+ }${
910
+ data[
911
+ item
912
+ .href
913
+ .key
914
+ ]
915
+ }`}
916
+ >
917
+ {renderValue(
918
+ item,
919
+ data
920
+ )}
921
+ </Link>
922
+ ) : (
923
+ <>
924
+ {renderValue(
925
+ item,
926
+ data
927
+ )}
928
+ </>
929
+ )}
930
+ </li>
931
+ )
932
+ )}
933
+ </ul>
934
+ )}
935
+ </React.Fragment>
936
+ )
937
+ )}
938
+ </div>
939
+ {activeTabConfig.form && (
940
+ <div className={styles.polActions}>
941
+ <button
942
+ className={styles.btn}
943
+ onClick={() =>
944
+ modalOpen(
945
+ 'update',
946
+ routeParams[urlParam]
947
+ )
948
+ }
949
+ >
950
+ Edit
951
+ </button>
952
+ </div>
953
+ )}
954
+ </>
955
+ );
956
+ case 'qrCode':
957
+ return (
958
+ <QrCode
959
+ config={activeTabConfig}
960
+ dataId={routeParams[urlParam]}
961
+ />
962
+ );
963
+ case 'table':
964
+ if (
965
+ config.ajaxSetting &&
966
+ config.form &&
967
+ config.columns &&
968
+ config.settings
969
+ ) {
970
+ return (
971
+ <>
972
+ {activeTabConfig.dropzone &&
973
+ activeTabConfig.dropzone
974
+ .relationship &&
975
+ activeTabConfig.dropzone.url !==
976
+ '' && (
977
+ <div
978
+ className={styles.grid__row}
979
+ >
980
+ <div
981
+ className={
982
+ styles.grid__full
983
+ }
984
+ >
985
+ <Dropzone
986
+ onDrop={(
987
+ acceptedFiles
988
+ ) => {
989
+ const uploadFile =
990
+ (file) => {
991
+ Vapor.store(
992
+ file,
993
+ {
994
+ progress:
995
+ (
996
+ progress
997
+ ) => {
998
+ setLoadingProgress(
999
+ progress *
1000
+ 100
1001
+ );
1002
+ },
1003
+ }
1004
+ ).then(
1005
+ (
1006
+ response
1007
+ ) => {
1008
+ setLoadingProgress(
1009
+ 0
1010
+ );
1011
+ const requestBody =
1012
+ {
1013
+ uuid: response.uuid,
1014
+ key: response.key,
1015
+ bucket: response.bucket,
1016
+ name: file.name,
1017
+ size: file.size,
1018
+ extension:
1019
+ response.extension,
1020
+ file_relationship:
1021
+ activeTabConfig
1022
+ .dropzone
1023
+ .relationship,
1024
+ };
1025
+
1026
+ CustomFetch(
1027
+ `${activeTabConfig.dropzone.url}/${routeParams[urlParam]}`,
1028
+ 'POST',
1029
+ requestBody,
1030
+ (
1031
+ res
1032
+ ) => {
1033
+ if (
1034
+ res.error ===
1035
+ ''
1036
+ ) {
1037
+ gridRef.current.reload();
1038
+ } else {
1039
+ toast.error(
1040
+ res.error
1041
+ );
1042
+ }
1043
+ }
1044
+ );
1045
+ }
1046
+ );
1047
+ };
1048
+
1049
+ acceptedFiles.forEach(
1050
+ (a) => {
1051
+ uploadFile(
1052
+ a
1053
+ );
1054
+ }
1055
+ );
1056
+ }}
1057
+ >
1058
+ {({
1059
+ getRootProps,
1060
+ getInputProps,
1061
+ isDragActive,
1062
+ }) => (
1063
+ <section
1064
+ className={
1065
+ styles.dropzone__container
1066
+ }
1067
+ >
1068
+ <div
1069
+ {...getRootProps()}
1070
+ >
1071
+ <input
1072
+ {...getInputProps()}
1073
+ />
1074
+ {isDragActive ? (
1075
+ <p>
1076
+ Drop
1077
+ the
1078
+ files
1079
+ here
1080
+ ...
1081
+ </p>
1082
+ ) : (
1083
+ <p>
1084
+ Drag
1085
+ 'n'
1086
+ drop
1087
+ some
1088
+ files
1089
+ here,
1090
+ or
1091
+ click
1092
+ to
1093
+ select
1094
+ files
1095
+ </p>
1096
+ )}
1097
+ </div>
1098
+ </section>
1099
+ )}
1100
+ </Dropzone>
1101
+ <div
1102
+ className={
1103
+ styles.progress
1104
+ }
1105
+ >
1106
+ <motion.div
1107
+ className={
1108
+ styles.progress__bar
1109
+ }
1110
+ initial={{
1111
+ width: '0%',
1112
+ }}
1113
+ animate={{
1114
+ width:
1115
+ loadingProgress +
1116
+ '%',
1117
+ }}
1118
+ transition={{
1119
+ duration: 2,
1120
+ ease: [
1121
+ 0.165,
1122
+ 0.84,
1123
+ 0.44,
1124
+ 1.0,
1125
+ ],
1126
+ }}
1127
+ />
1128
+ </div>
1129
+ </div>
1130
+ </div>
1131
+ )}
1132
+ <div className={styles.grid__row}>
1133
+ <div className={styles.grid__full}>
1134
+ <Table
1135
+ {...config}
1136
+ ref={gridRef}
1137
+ gridHeight={
1138
+ windowHeight * 0.65
1139
+ }
1140
+ pageData={
1141
+ data ? data : null
1142
+ }
1143
+ pageSetting={
1144
+ page ? page : null
1145
+ }
1146
+ paramValue={
1147
+ routeParams[urlParam]
1148
+ ? routeParams[
1149
+ urlParam
1150
+ ]
1151
+ : null
1152
+ }
1153
+ setConfig={setConfig}
1154
+ setTotal={setTotal}
1155
+ style={
1156
+ userProfile?.settings
1157
+ ?.style || {}
1158
+ }
1159
+ />
1160
+ </div>
1161
+ </div>
1162
+
1163
+ {activeTabConfig.functions
1164
+ ?.checkboxUpdate && (
1165
+ <div className={styles.polActions}>
1166
+ <button
1167
+ className={styles.btn}
1168
+ onClick={
1169
+ handleCheckboxUpdate
1170
+ }
1171
+ >
1172
+ {activeTabConfig.functions
1173
+ .checkboxUpdate.label ||
1174
+ 'Update'}
1175
+ </button>
1176
+ </div>
1177
+ )}
1178
+ </>
1179
+ );
1180
+ } else {
1181
+ return null;
1182
+ }
1183
+ default:
1184
+ return null;
1185
+ }
1186
+ }
1187
+ }
1188
+ }
1189
+ };
1190
+
1191
+ /** General Hooks */
1192
+ useEffect(() => {
1193
+ const handleResize = () => {
1194
+ setWindowHeight(window.innerHeight);
1195
+ };
1196
+
1197
+ window.addEventListener('resize', handleResize);
1198
+
1199
+ // Clean up the event listener
1200
+ return () => {
1201
+ window.removeEventListener('resize', handleResize);
1202
+ };
1203
+ }, []);
1204
+
1205
+ useEffect(() => {
1206
+ if (tabs && tabs.length > 0) {
1207
+ const activeTab = subnav.find((s) => s.show === true);
1208
+
1209
+ if (activeTab) {
1210
+ const activeTabConfig = tabs.find((t) => t.id === activeTab.id);
1211
+
1212
+ const applyUrlParamsToData = (data) => {
1213
+ return data.map((item) => {
1214
+ if (item.hasOwnProperty('value') && item.urlParam) {
1215
+ return {
1216
+ ...item,
1217
+ value:
1218
+ item.hasOwnProperty('value') &&
1219
+ item.urlParam &&
1220
+ routeParams[item.urlParam]
1221
+ ? routeParams[item.urlParam]
1222
+ : item.value,
1223
+ };
1224
+ } else {
1225
+ return item;
1226
+ }
1227
+ });
1228
+ };
1229
+
1230
+ if (activeTabConfig) {
1231
+ switch (activeTabConfig.type) {
1232
+ case 'table':
1233
+ if (
1234
+ activeTabConfig.ajaxSetting &&
1235
+ activeTabConfig.ajaxSetting.where
1236
+ ) {
1237
+ const updatedAjaxSetting = {
1238
+ ...activeTabConfig.ajaxSetting,
1239
+ where: applyUrlParamsToData(
1240
+ activeTabConfig.ajaxSetting.where
1241
+ ),
1242
+ };
1243
+
1244
+ const updatedForm = {
1245
+ ...activeTabConfig.form,
1246
+ fields: activeTabConfig.form.fields.map(
1247
+ (field) => {
1248
+ if (
1249
+ field.hasOwnProperty('value') &&
1250
+ field.urlParam &&
1251
+ routeParams[field.urlParam]
1252
+ ) {
1253
+ if (
1254
+ field.type &&
1255
+ field.relationName &&
1256
+ (field.type ===
1257
+ 'multi-dropdown-ajax' ||
1258
+ field.type ===
1259
+ 'multi-dropdown')
1260
+ ) {
1261
+ field.value = {
1262
+ value: routeParams[
1263
+ field.urlParam
1264
+ ],
1265
+ label: data[
1266
+ field.relationName
1267
+ ],
1268
+ };
1269
+ } else {
1270
+ field.value =
1271
+ routeParams[
1272
+ field.urlParam
1273
+ ];
1274
+ }
1275
+ }
1276
+
1277
+ if (field.where) {
1278
+ return {
1279
+ ...field,
1280
+ where: applyUrlParamsToData(
1281
+ field.where
1282
+ ),
1283
+ };
1284
+ } else {
1285
+ return field;
1286
+ }
1287
+ }
1288
+ ),
1289
+ };
1290
+
1291
+ setConfig({
1292
+ ajaxSetting: updatedAjaxSetting,
1293
+ form: updatedForm,
1294
+ columns: activeTabConfig.columns,
1295
+ settings: activeTabConfig.settings,
1296
+ ...(activeTabConfig.tableSetting && {
1297
+ tableSetting:
1298
+ activeTabConfig.tableSetting,
1299
+ }),
1300
+ ...(activeTabConfig.functions && {
1301
+ functions: activeTabConfig.functions,
1302
+ }),
1303
+ ...(setRowsSelected && {
1304
+ setRowsSelected: setRowsSelected,
1305
+ }),
1306
+ });
1307
+ }
1308
+ break;
1309
+ case 'overview':
1310
+ setConfig({});
1311
+ if (activeTabConfig.form) {
1312
+ setFormData(activeTabConfig.form);
1313
+ }
1314
+ break;
1315
+ default:
1316
+ setConfig({});
1317
+ break;
1318
+ }
1319
+ }
1320
+ }
1321
+ }
1322
+ }, [subnav, tabs, routeParams]);
1323
+
1324
+ useEffect(() => {
1325
+ if (tabs && tabs.length > 0 && data && data.id) {
1326
+ const activeTab = subnav.find((s) => s.show === true);
1327
+
1328
+ if (activeTab) {
1329
+ const activeTabConfig = tabs.find((t) => t.id === activeTab.id);
1330
+
1331
+ if (activeTabConfig?.stages?.key) {
1332
+ setActiveStage(data[activeTabConfig.stages.key]);
1333
+ }
1334
+ }
1335
+ }
1336
+ }, [data, tabs]);
1337
+
1338
+ useEffect(() => {
1339
+ if (routeParams[urlParam] && routeParams[urlParam] > 0) {
1340
+ CustomFetch(
1341
+ `${page.fetchUrl}/${routeParams[urlParam]}`,
1342
+ 'GET',
1343
+ {},
1344
+ (res) => {
1345
+ setData(res);
1346
+
1347
+ tabs.forEach((tab) => {
1348
+ if (tab.type && tab.type === 'dropzone') {
1349
+ if (res.files) {
1350
+ setFiles(res.files);
1351
+ }
1352
+ }
1353
+ });
1354
+ }
1355
+ );
1356
+ }
1357
+ }, [routeParams['*'], dataReload]);
1358
+
1359
+ useEffect(() => {
1360
+ setSubnav(() => {
1361
+ return filters.map((f) => {
1362
+ if (
1363
+ f.hasOwnProperty('url') &&
1364
+ f.baseUrl &&
1365
+ f.urlParam &&
1366
+ routeParams[f.urlParam]
1367
+ ) {
1368
+ return {
1369
+ ...f,
1370
+ url: `${f.baseUrl}${routeParams[f.urlParam]}`,
1371
+ };
1372
+ } else {
1373
+ return f;
1374
+ }
1375
+ });
1376
+ });
1377
+ }, [filters]);
1378
+
1379
+ /** Modal States */
1380
+ const [formData, setFormData] = useState({});
1381
+ const [formType, setFormType] = useState('');
1382
+ const [formId, setFormId] = useState(0);
1383
+ const [modalShow, setModalShow] = useState(false);
1384
+
1385
+ /** Modal Functions */
1386
+ const modalOpen = (formType, formId) => {
1387
+ setModalShow(true);
1388
+ setFormType(formType);
1389
+ setFormId(formId);
1390
+ };
1391
+
1392
+ const modalClose = () => {
1393
+ setModalShow(false);
1394
+ };
1395
+
1396
+ return (
1397
+ <>
1398
+ <div className={styles.grid}>
1399
+ <div className={styles.grid__row}>
1400
+ <div
1401
+ className={`${styles.grid__full} ${
1402
+ layout === 'full'
1403
+ ? styles.crmtitle
1404
+ : styles['crmtitle--alternate']
1405
+ }`}
1406
+ >
1407
+ <Breadcrumb data={data} page={page} />
1408
+ {total > 0 && (
1409
+ <div className={styles.titleInfo}>
1410
+ <span>
1411
+ [<strong>{total}</strong> Total]
1412
+ </span>
1413
+ </div>
1414
+ )}
1415
+ </div>
1416
+ </div>
1417
+ </div>
1418
+
1419
+ {stages?.data?.length > 0 && (
1420
+ <div className={styles.grid}>
1421
+ <div className={styles.grid__row}>
1422
+ <div className={styles.grid__full}>
1423
+ <div className={styles.oppstatus}>
1424
+ <ul className={styles.opppath}>
1425
+ {stages.data.map((stage) => {
1426
+ if (!stage.id) {
1427
+ return null; // Skip rendering this stage
1428
+ }
1429
+
1430
+ const isStageComplete =
1431
+ data[stages.key] >= stage.id;
1432
+
1433
+ return (
1434
+ <li
1435
+ key={`stage-${stage.id}`}
1436
+ className={
1437
+ isStageComplete
1438
+ ? `${styles['opp-complete']}`
1439
+ : ''
1440
+ }
1441
+ onClick={(e) => {
1442
+ e.preventDefault();
1443
+
1444
+ handleStageUpdate(
1445
+ stage,
1446
+ stages.type
1447
+ );
1448
+ }}
1449
+ >
1450
+ <a>
1451
+ {stage.label}
1452
+ {isStageComplete && (
1453
+ <CircleCheck
1454
+ strokeWidth={2}
1455
+ size={16}
1456
+ />
1457
+ )}
1458
+ </a>
1459
+ </li>
1460
+ );
1461
+ })}
1462
+ </ul>
1463
+ </div>
1464
+ </div>
1465
+ </div>
1466
+ </div>
1467
+ )}
1468
+
1469
+ <div className={styles.grid}>
1470
+ <div className={styles.grid__subrow}>
1471
+ <div className={styles.grid__subnav}>
1472
+ <TableFilter filters={subnav} setFilters={setSubnav} />
1473
+ </div>
1474
+ <div className={styles.grid__subcontent}>
1475
+ {config ? renderContent() : null}
1476
+ </div>
1477
+ </div>
1478
+ </div>
1479
+
1480
+ <Popup
1481
+ open={modalShow}
1482
+ onClose={modalClose}
1483
+ closeOnDocumentClick={false}
1484
+ >
1485
+ <Form
1486
+ closeModal={modalClose}
1487
+ columnId={formId}
1488
+ fetchTable={handleReload}
1489
+ formSettings={formData}
1490
+ formType={formType}
1491
+ style={userProfile?.settings?.style || {}}
1492
+ updateForm={setFormData}
1493
+ />
1494
+ </Popup>
1495
+ </>
1496
+ );
1497
+ }
1498
+
1499
+ export default GenericDetail;