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