@visns-studio/visns-components 5.0.18 → 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 +8 -6
- package/src/components/crm/Breadcrumb.jsx +22 -10
- package/src/components/crm/MultiSelect.jsx +46 -67
- package/src/components/crm/Navigation.jsx +0 -33
- 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/GenericDetail.jsx +53 -0
- 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;
|
|
@@ -2,6 +2,7 @@ import '../../styles/global.css';
|
|
|
2
2
|
|
|
3
3
|
import React, { useEffect, useRef, useState } from 'react';
|
|
4
4
|
import { Link, useParams } from 'react-router-dom';
|
|
5
|
+
import { Calendar, momentLocalizer } from 'react-big-calendar';
|
|
5
6
|
import moment from 'moment';
|
|
6
7
|
import parse from 'html-react-parser';
|
|
7
8
|
import Popup from 'reactjs-popup';
|
|
@@ -23,6 +24,7 @@ import {
|
|
|
23
24
|
} from 'akar-icons';
|
|
24
25
|
|
|
25
26
|
import 'react-confirm-alert/src/react-confirm-alert.css';
|
|
27
|
+
import 'react-big-calendar/lib/css/react-big-calendar.css';
|
|
26
28
|
|
|
27
29
|
import Breadcrumb from '../Breadcrumb';
|
|
28
30
|
import CustomFetch from '../Fetch';
|
|
@@ -36,6 +38,8 @@ import TableFilter from '../TableFilter';
|
|
|
36
38
|
|
|
37
39
|
import styles from './styles/GenericDetail.module.scss';
|
|
38
40
|
|
|
41
|
+
const localizer = momentLocalizer(moment);
|
|
42
|
+
|
|
39
43
|
function GenericDetail({
|
|
40
44
|
extraUrlParam,
|
|
41
45
|
layout = 'full',
|
|
@@ -712,6 +716,55 @@ function GenericDetail({
|
|
|
712
716
|
|
|
713
717
|
if (activeTabConfig && activeTabConfig.type) {
|
|
714
718
|
switch (activeTabConfig.type) {
|
|
719
|
+
case 'calendar':
|
|
720
|
+
const events =
|
|
721
|
+
data?.[activeTabConfig?.dataKey]
|
|
722
|
+
?.map((event) => {
|
|
723
|
+
// Safely access keys using optional chaining and defaults
|
|
724
|
+
const keys =
|
|
725
|
+
activeTabConfig?.config?.keys || {};
|
|
726
|
+
const eventStart = event?.[keys.start]
|
|
727
|
+
? moment(event[keys.start])
|
|
728
|
+
: null;
|
|
729
|
+
const eventEnd = event?.[keys.end]
|
|
730
|
+
? moment(event[keys.end])
|
|
731
|
+
: null;
|
|
732
|
+
|
|
733
|
+
// Only include valid events
|
|
734
|
+
if (eventStart && eventEnd) {
|
|
735
|
+
return {
|
|
736
|
+
title:
|
|
737
|
+
event?.[keys.title] ||
|
|
738
|
+
'Untitled Event', // Default title if not provided
|
|
739
|
+
start: eventStart,
|
|
740
|
+
end: eventEnd,
|
|
741
|
+
};
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
return null; // Filter out invalid events
|
|
745
|
+
})
|
|
746
|
+
?.filter(Boolean) || [];
|
|
747
|
+
|
|
748
|
+
return (
|
|
749
|
+
<Calendar
|
|
750
|
+
localizer={localizer}
|
|
751
|
+
events={events}
|
|
752
|
+
startAccessor="start"
|
|
753
|
+
endAccessor="end"
|
|
754
|
+
style={{
|
|
755
|
+
height:
|
|
756
|
+
activeTabConfig.config.height ||
|
|
757
|
+
500,
|
|
758
|
+
}}
|
|
759
|
+
views={
|
|
760
|
+
activeTabConfig.config.views || [
|
|
761
|
+
'month',
|
|
762
|
+
'week',
|
|
763
|
+
'day',
|
|
764
|
+
]
|
|
765
|
+
}
|
|
766
|
+
/>
|
|
767
|
+
);
|
|
715
768
|
case 'dropzone':
|
|
716
769
|
const downloadFile = (file) => {
|
|
717
770
|
const { id, file_url, file_name } = file;
|