@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.
- package/package.json +6 -6
- package/src/components/crm/MultiSelect.jsx +46 -67
- package/src/components/crm/auth/styles/Profile.module.scss +6 -7
- package/src/components/crm/generic/GenericDashboard.jsx +495 -29
- package/src/components/crm/generic/GenericFormBuilder.jsx +96 -32
- package/src/components/crm/generic/styles/AuditLog.module.scss +3 -3
- package/src/components/crm/generic/styles/AuditLogs.module.scss +1 -1
- package/src/components/crm/generic/styles/GenericDashboard.module.scss +364 -0
- package/src/components/crm/generic/styles/GenericDynamic.module.scss +11 -11
- package/src/components/crm/generic/styles/GenericFormBuilder.module.scss +327 -317
- package/src/components/crm/generic/styles/GenericIndex.module.scss +8 -8
- package/src/components/crm/generic/styles/GenericSort.module.scss +3 -3
- package/src/components/crm/generic/styles/NotificationList.module.scss +2 -2
- package/src/components/crm/sketch/SketchField.jsx +395 -0
- package/src/components/crm/sketch/arrow.jsx +108 -0
- package/src/components/crm/sketch/circle.jsx +75 -0
- package/src/components/crm/sketch/default-tool.jsx +16 -0
- package/src/components/crm/sketch/fabrictool.jsx +22 -0
- package/src/components/crm/sketch/history.jsx +144 -0
- package/src/components/crm/sketch/json/config.json +14 -0
- package/src/components/crm/sketch/line.jsx +64 -0
- package/src/components/crm/sketch/pan.jsx +48 -0
- package/src/components/crm/sketch/pencil.jsx +36 -0
- package/src/components/crm/sketch/rectangle-label-object.jsx +69 -0
- package/src/components/crm/sketch/rectangle-label.jsx +93 -0
- package/src/components/crm/sketch/rectangle.jsx +76 -0
- package/src/components/crm/sketch/select.jsx +16 -0
- package/src/components/crm/sketch/tools.jsx +11 -0
- package/src/components/crm/sketch/utils.jsx +67 -0
- package/src/components/crm/sorting/Item.jsx +2 -2
- package/src/components/crm/sorting/styles/Item.module.scss +12 -65
- package/src/components/crm/sorting/styles/List.module.scss +6 -20
- package/src/components/crm/styles/Form.module.scss +4 -6
- package/src/components/crm/styles/MultiSelect.module.scss +4 -0
- package/src/components/crm/styles/Navigation.module.scss +13 -6
- package/src/components/crm/styles/QrCode.module.scss +2 -2
- package/src/components/styles/global.css +8 -0
- 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 {
|
|
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
|
-
|
|
6
|
-
|
|
14
|
+
import styles from './styles/GenericDashboard.module.scss';
|
|
15
|
+
import { drop } from 'lodash';
|
|
7
16
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
34
|
-
|
|
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
|
-
</
|
|
503
|
+
</Popup>
|
|
38
504
|
</div>
|
|
39
505
|
);
|
|
40
|
-
}
|
|
506
|
+
}
|
|
41
507
|
|
|
42
508
|
export default GenericDashboard;
|