@visns-studio/visns-components 5.0.19 → 5.0.21

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 (38) hide show
  1. package/package.json +6 -6
  2. package/src/components/crm/MultiSelect.jsx +46 -67
  3. package/src/components/crm/auth/styles/Profile.module.scss +6 -7
  4. package/src/components/crm/generic/GenericDashboard.jsx +495 -29
  5. package/src/components/crm/generic/GenericFormBuilder.jsx +96 -32
  6. package/src/components/crm/generic/styles/AuditLog.module.scss +3 -3
  7. package/src/components/crm/generic/styles/AuditLogs.module.scss +1 -1
  8. package/src/components/crm/generic/styles/GenericDashboard.module.scss +364 -0
  9. package/src/components/crm/generic/styles/GenericDynamic.module.scss +11 -11
  10. package/src/components/crm/generic/styles/GenericFormBuilder.module.scss +327 -317
  11. package/src/components/crm/generic/styles/GenericIndex.module.scss +8 -8
  12. package/src/components/crm/generic/styles/GenericSort.module.scss +3 -3
  13. package/src/components/crm/generic/styles/NotificationList.module.scss +2 -2
  14. package/src/components/crm/sketch/SketchField.jsx +395 -0
  15. package/src/components/crm/sketch/arrow.jsx +108 -0
  16. package/src/components/crm/sketch/circle.jsx +75 -0
  17. package/src/components/crm/sketch/default-tool.jsx +16 -0
  18. package/src/components/crm/sketch/fabrictool.jsx +22 -0
  19. package/src/components/crm/sketch/history.jsx +144 -0
  20. package/src/components/crm/sketch/json/config.json +14 -0
  21. package/src/components/crm/sketch/line.jsx +64 -0
  22. package/src/components/crm/sketch/pan.jsx +48 -0
  23. package/src/components/crm/sketch/pencil.jsx +36 -0
  24. package/src/components/crm/sketch/rectangle-label-object.jsx +69 -0
  25. package/src/components/crm/sketch/rectangle-label.jsx +93 -0
  26. package/src/components/crm/sketch/rectangle.jsx +76 -0
  27. package/src/components/crm/sketch/select.jsx +16 -0
  28. package/src/components/crm/sketch/tools.jsx +11 -0
  29. package/src/components/crm/sketch/utils.jsx +67 -0
  30. package/src/components/crm/sorting/Item.jsx +2 -2
  31. package/src/components/crm/sorting/styles/Item.module.scss +12 -65
  32. package/src/components/crm/sorting/styles/List.module.scss +6 -20
  33. package/src/components/crm/styles/Form.module.scss +4 -6
  34. package/src/components/crm/styles/MultiSelect.module.scss +4 -0
  35. package/src/components/crm/styles/Navigation.module.scss +13 -6
  36. package/src/components/crm/styles/QrCode.module.scss +2 -2
  37. package/src/components/styles/global.css +8 -0
  38. package/src/index.js +2 -0
@@ -1,42 +1,508 @@
1
+ import '../../styles/global.css';
2
+
1
3
  import React, { useState, useEffect } from 'react';
2
- import { toast } from 'react-toastify';
4
+ import { Link } from 'react-router-dom';
5
+ import { ResponsiveBar } from '@nivo/bar';
6
+ import { ResponsiveLine } from '@nivo/line';
7
+ import { ResponsivePie } from '@nivo/pie';
8
+ import Popup from 'reactjs-popup';
9
+ import { CircleX } from 'akar-icons';
10
+
3
11
  import CustomFetch from '../../crm/Fetch';
12
+ import MultiSelect from '../../crm/MultiSelect';
4
13
 
5
- const GenericDashboard = ({ userProfile }) => {
6
- const [data, setData] = useState([]);
14
+ import styles from './styles/GenericDashboard.module.scss';
15
+ import { drop } from 'lodash';
7
16
 
8
- return (
9
- <div className="grid">
10
- <div className="grid__row">
11
- <div className="grid__three dashtopwidge">
12
- <div className="dashtopwidge__left">
13
- <h2>
14
- Current
15
- <span>
16
- <strong>Enquiries</strong>
17
- </span>
18
- </h2>
19
- </div>
20
- <div className="dashtopwidge__right">
21
- <span>0</span>
17
+ const toSnakeCase = (str) => str.replace(/[\s-]+/g, '_').toLowerCase();
18
+
19
+ function GenericDashboard({ setting }) {
20
+ const [data, setData] = useState({});
21
+ const [dropdowns, setDropdowns] = useState({});
22
+ const [filters, setFilters] = useState({}); // Track filter values for each widget
23
+ const [modalShow, setModalShow] = useState(false);
24
+ const [modalContent, setModalContent] = useState(null);
25
+
26
+ const fetchData = async (appliedFilters = {}) => {
27
+ try {
28
+ const promises = setting.widgets.map(async (widget) => {
29
+ const widgetFilters = appliedFilters[widget.id] || {};
30
+
31
+ // Fetch dropdown data if required
32
+ if (widget.filters) {
33
+ for (const filter of widget.filters) {
34
+ if (filter.url) {
35
+ const initialFilterData = await CustomFetch(
36
+ filter.url,
37
+ 'POST',
38
+ filter.defaultParams || {}
39
+ );
40
+ setDropdowns((prevFilters) => ({
41
+ ...prevFilters,
42
+ [widget.id]: {
43
+ ...prevFilters[widget.id],
44
+ [filter.id]:
45
+ initialFilterData?.data?.data || [],
46
+ },
47
+ }));
48
+ }
49
+ }
50
+ }
51
+
52
+ // Fetch widget data with filters
53
+ return CustomFetch(
54
+ widget.api.url,
55
+ widget.api.method,
56
+ widgetFilters
57
+ );
58
+ });
59
+
60
+ const results = await Promise.all(promises);
61
+
62
+ const fetchedData = setting.widgets.reduce((acc, widget, index) => {
63
+ acc[widget.id] = results[index]?.data;
64
+ return acc;
65
+ }, {});
66
+
67
+ setData(fetchedData);
68
+ } catch (error) {
69
+ console.error('Error fetching dashboard data:', error);
70
+ }
71
+ };
72
+
73
+ useEffect(() => {
74
+ fetchData(); // Fetch initial data
75
+ }, [setting]);
76
+
77
+ const openModal = (widgetId) => {
78
+ setModalContent(widgetId);
79
+ setModalShow(true);
80
+ };
81
+
82
+ const closeModal = () => {
83
+ setModalShow(false);
84
+ };
85
+
86
+ const handleFilterChange = (widgetId, key, value) => {
87
+ setFilters((prevFilters) => ({
88
+ ...prevFilters,
89
+ [widgetId]: {
90
+ ...prevFilters[widgetId],
91
+ [key]: value,
92
+ },
93
+ }));
94
+ };
95
+
96
+ const handleFilterSelectChange = (value, action, id) => {
97
+ const widgetId = id.split('.')[0];
98
+ const filterId = id.split('.')[1];
99
+
100
+ if (action.action === 'select-option') {
101
+ setFilters((prevFilters) => ({
102
+ ...prevFilters,
103
+ [widgetId]: {
104
+ ...prevFilters[widgetId],
105
+ [filterId]: value,
106
+ },
107
+ }));
108
+ } else if (action.action === 'clear') {
109
+ setFilters((prevFilters) => ({
110
+ ...prevFilters,
111
+ [widgetId]: {
112
+ ...prevFilters[widgetId],
113
+ [filterId]: [],
114
+ },
115
+ }));
116
+ } else if (action.action === 'remove-value') {
117
+ setFilters((prevFilters) => ({
118
+ ...prevFilters,
119
+ [widgetId]: {
120
+ ...prevFilters[widgetId],
121
+ [filterId]: value,
122
+ },
123
+ }));
124
+ }
125
+ };
126
+
127
+ const renderFilters = (widget) => {
128
+ if (!widget.filters || widget.filters.length === 0) return null;
129
+
130
+ return (
131
+ <div className={styles['filter-container']}>
132
+ <div className={styles['filter-header']}>
133
+ <h3>Filters</h3>
134
+ </div>
135
+ <div className={styles['filter-fields']}>
136
+ {widget.filters.map((filter) => {
137
+ switch (filter.type) {
138
+ case 'date':
139
+ return (
140
+ <div
141
+ className={styles['filter-field']}
142
+ key={filter.id}
143
+ >
144
+ <label htmlFor={filter.id}>
145
+ {filter.label}
146
+ </label>
147
+ <input
148
+ id={filter.id}
149
+ type="date"
150
+ value={
151
+ filters[widget.id]?.[
152
+ filter.id
153
+ ] || ''
154
+ }
155
+ onChange={(e) =>
156
+ handleFilterChange(
157
+ widget.id,
158
+ filter.id,
159
+ e.target.value
160
+ )
161
+ }
162
+ />
163
+ </div>
164
+ );
165
+ case 'multi-dropdown-ajax':
166
+ return (
167
+ <div
168
+ className={styles['filter-field']}
169
+ key={filter.id}
170
+ >
171
+ <label htmlFor={filter.id}>
172
+ {filter.label}
173
+ </label>
174
+ <MultiSelect
175
+ settings={{
176
+ id: `${widget.id}.${filter.id}`,
177
+ }}
178
+ multi={true}
179
+ options={
180
+ dropdowns[widget.id]?.[
181
+ filter.id
182
+ ] || []
183
+ }
184
+ inputValue={
185
+ filters[widget.id]?.[
186
+ filter.id
187
+ ] || []
188
+ }
189
+ onChange={handleFilterSelectChange}
190
+ style={{
191
+ multi_select_height: '52px',
192
+ }}
193
+ />
194
+ </div>
195
+ );
196
+ default:
197
+ return null;
198
+ }
199
+ })}
200
+ <div className={styles['filter-actions']}>
201
+ <button
202
+ className={`${styles.btn} ${styles['btn-primary']}`}
203
+ onClick={() => {
204
+ fetchData(filters); // Trigger fetchData with the current filters
205
+ }}
206
+ >
207
+ Apply Filters
208
+ </button>
209
+ <button
210
+ className={`${styles.btn} ${styles['btn-secondary']}`}
211
+ onClick={() => {
212
+ setFilters((prevFilters) => ({
213
+ ...prevFilters,
214
+ [widget.id]: {},
215
+ }));
216
+
217
+ fetchData(); // Reset filters and fetch data
218
+ }}
219
+ >
220
+ Clear Filters
221
+ </button>
22
222
  </div>
23
223
  </div>
24
- <div className="grid__three dashtopwidge">
25
- <div className="dashtopwidge__left">
26
- <h2>
27
- Current{' '}
28
- <span>
29
- <strong>Enquiries</strong>
30
- </span>
31
- </h2>
224
+ </div>
225
+ );
226
+ };
227
+
228
+ const renderWidget = (widget) => {
229
+ const widgetData = data[widget.id] || [];
230
+
231
+ if (widgetData.length > 0 || widgetData?.data?.length > 0) {
232
+ switch (widget.type) {
233
+ case 'pie':
234
+ return (
235
+ <ResponsivePie data={widgetData} {...widget.props} />
236
+ );
237
+ case 'bar':
238
+ if (widgetData.keys && widget.props.keys) {
239
+ widget.props.keys = widgetData.keys;
240
+ }
241
+
242
+ const barData = widgetData.bar?.data || widgetData;
243
+
244
+ return (
245
+ <div style={{ height: '600px' }}>
246
+ <ResponsiveBar data={barData} {...widget.props} />
247
+ </div>
248
+ );
249
+ case 'line':
250
+ return (
251
+ <ResponsiveLine data={widgetData} {...widget.props} />
252
+ );
253
+ case 'list':
254
+ return (
255
+ <ul className={styles.dashList}>
256
+ {widgetData.length > 0 ? (
257
+ widgetData
258
+ .slice(0, widget.limit || 4)
259
+ .map((item) => (
260
+ <li key={`${widget.id}-${item.id}`}>
261
+ <Link
262
+ to={`${widget.link.base}/$${
263
+ item[widget.link.key]
264
+ }`}
265
+ >
266
+ <button>
267
+ <strong>
268
+ {widget?.item?.label &&
269
+ item[widget.item.label]
270
+ ? item[
271
+ widget.item
272
+ .label
273
+ ]
274
+ : ''}
275
+ </strong>
276
+ {widget.showDate && (
277
+ <span>{item.date}</span>
278
+ )}
279
+ </button>
280
+ </Link>
281
+ </li>
282
+ ))
283
+ ) : (
284
+ <li>
285
+ <button>
286
+ <strong>{widget.emptyMessage}</strong>
287
+ </button>
288
+ </li>
289
+ )}
290
+ </ul>
291
+ );
292
+ case 'table':
293
+ const tableData = widgetData.data || widgetData;
294
+ const tableKeys = widgetData.keys || [];
295
+
296
+ return (
297
+ <div className={styles['table-container']}>
298
+ {tableData.length > 0 ? (
299
+ <div className={styles['table-scroll']}>
300
+ <table>
301
+ <thead>
302
+ <tr>
303
+ <th>Form</th>
304
+ {tableData.map((row, index) => (
305
+ <th
306
+ key={`${toSnakeCase(
307
+ row.supervisor_name
308
+ )}-${index}`}
309
+ >
310
+ {row.supervisor_name}
311
+ </th>
312
+ ))}
313
+ <th>Total</th>
314
+ </tr>
315
+ </thead>
316
+ <tbody>
317
+ {tableKeys.map(
318
+ (type, typeIndex) => {
319
+ let total = 0; // Track total for the current form type
320
+ return (
321
+ <tr
322
+ key={`form-type-${toSnakeCase(
323
+ type
324
+ )}`}
325
+ >
326
+ <td>{type}</td>
327
+ {tableData.map(
328
+ (
329
+ row,
330
+ rowIndex
331
+ ) => {
332
+ const form =
333
+ row.forms.find(
334
+ (
335
+ form
336
+ ) =>
337
+ form.form_template_label ===
338
+ type
339
+ );
340
+
341
+ const count =
342
+ form
343
+ ? form.form_count
344
+ : 0;
345
+
346
+ total +=
347
+ count;
348
+
349
+ return (
350
+ <td
351
+ key={`${toSnakeCase(
352
+ row.supervisor_name
353
+ )}-${typeIndex}-${rowIndex}`}
354
+ >
355
+ {
356
+ count
357
+ }
358
+ </td>
359
+ );
360
+ }
361
+ )}
362
+ <td>
363
+ <strong>
364
+ {total}
365
+ </strong>
366
+ </td>
367
+ </tr>
368
+ );
369
+ }
370
+ )}
371
+ {/* Add a total row for supervisors */}
372
+ <tr>
373
+ <td>
374
+ <strong>Total</strong>
375
+ </td>
376
+ {tableData.map(
377
+ (row, rowIndex) => {
378
+ const supervisorTotal =
379
+ tableKeys.reduce(
380
+ (sum, type) => {
381
+ const form =
382
+ row.forms.find(
383
+ (
384
+ form
385
+ ) =>
386
+ form.form_template_label ===
387
+ type
388
+ );
389
+
390
+ return (
391
+ sum +
392
+ (form
393
+ ? form.form_count
394
+ : 0)
395
+ );
396
+ },
397
+ 0
398
+ );
399
+
400
+ return (
401
+ <td
402
+ key={`${toSnakeCase(
403
+ row.supervisor_name
404
+ )}-total-${rowIndex}`}
405
+ >
406
+ <strong>
407
+ {
408
+ supervisorTotal
409
+ }
410
+ </strong>
411
+ </td>
412
+ );
413
+ }
414
+ )}
415
+ <td />
416
+ </tr>
417
+ </tbody>
418
+ </table>
419
+ </div>
420
+ ) : (
421
+ <div>No data available</div>
422
+ )}
423
+ </div>
424
+ );
425
+ default:
426
+ return null;
427
+ }
428
+ } else {
429
+ return null;
430
+ }
431
+ };
432
+
433
+ const renderRows = () => {
434
+ const columns = setting.columns || 3; // Default to 3 columns if not specified
435
+ const widgetChunks = [];
436
+
437
+ for (let i = 0; i < setting.widgets.length; i += columns) {
438
+ widgetChunks.push(setting.widgets.slice(i, i + columns));
439
+ }
440
+
441
+ return widgetChunks.map((row, rowIndex) => (
442
+ <div className={styles.grid__row} key={`row-${rowIndex}`}>
443
+ {row.map((widget) => (
444
+ <div
445
+ key={widget.id}
446
+ className={`${styles.grid__full} ${
447
+ widget.type !== 'list'
448
+ ? widget.highlight
449
+ ? styles['dashwidget--highlight']
450
+ : styles.dashwidget
451
+ : null
452
+ }`}
453
+ >
454
+ <div className={styles.widgetTitle}>
455
+ <h2>{widget.title}</h2>
456
+ </div>
457
+ {renderFilters(widget)}
458
+ <div>{renderWidget(widget)}</div>
459
+ {widget.modal && (
460
+ <div className={styles.widgetAction}>
461
+ <button
462
+ className={`${styles.btn} ${styles.dmore}`}
463
+ onClick={() => openModal(widget.id)}
464
+ >
465
+ View all{' '}
466
+ <span>
467
+ ({data[widget.id]?.length || 0})
468
+ </span>
469
+ </button>
470
+ </div>
471
+ )}
32
472
  </div>
33
- <div className="dashtopwidge__right">
34
- <span>0</span>
473
+ ))}
474
+ </div>
475
+ ));
476
+ };
477
+
478
+ return (
479
+ <div className={styles.grid}>
480
+ {renderRows()}
481
+ <Popup open={modalShow} onClose={closeModal}>
482
+ <div className="modalwrap top--modal modalWide">
483
+ <div className="modal">
484
+ <div className="modal__header">
485
+ <h1>{setting.modalTitles?.[modalContent]}</h1>
486
+ <button
487
+ className="modal__close"
488
+ onClick={closeModal}
489
+ >
490
+ <CircleX strokeWidth={1} size={24} />
491
+ </button>
492
+ </div>
493
+ <div className="modal__content">
494
+ {modalContent &&
495
+ renderWidget(
496
+ setting.widgets.find(
497
+ (w) => w.id === modalContent
498
+ )
499
+ )}
500
+ </div>
35
501
  </div>
36
502
  </div>
37
- </div>
503
+ </Popup>
38
504
  </div>
39
505
  );
40
- };
506
+ }
41
507
 
42
508
  export default GenericDashboard;