@visns-studio/visns-components 5.14.0 → 5.14.2
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/README.md +241 -0
- package/package.json +3 -3
- package/src/components/generic/GenericDashboard.jsx +419 -55
- package/src/components/generic/GenericDetail.jsx +21 -7
- package/src/components/generic/OcrTemplateEnhanced.jsx +1321 -0
- package/src/components/generic/OcrTemplateEnhanced.md +249 -0
- package/src/components/generic/OcrTemplateEnhanced.module.scss +1226 -0
- package/src/components/styles/GenericDashboard.module.scss +236 -0
- package/src/index.js +2 -0
|
@@ -8,7 +8,7 @@ import Popup from 'reactjs-popup';
|
|
|
8
8
|
import dayjs from 'dayjs';
|
|
9
9
|
import parse from 'html-react-parser';
|
|
10
10
|
import { createSwapy } from 'swapy';
|
|
11
|
-
import { X, Minus, Plus, Star, Trash2 } from 'lucide-react';
|
|
11
|
+
import { X, Minus, Plus, Star, Trash2, Eye, RotateCcw, Clock, Database, BarChart3, AlertCircle } from 'lucide-react';
|
|
12
12
|
import { confirmDialog } from '../utils/ConfirmDialog';
|
|
13
13
|
|
|
14
14
|
import CustomFetch from '../Fetch';
|
|
@@ -18,6 +18,34 @@ import styles from '../styles/GenericDashboard.module.scss';
|
|
|
18
18
|
|
|
19
19
|
const toSnakeCase = (str) => str.replace(/[\s-]+/g, '_').toLowerCase();
|
|
20
20
|
|
|
21
|
+
// Helper function to format dates for display in Australian format
|
|
22
|
+
const formatDateForDisplay = (dateString) => {
|
|
23
|
+
if (!dateString || dateString === 'Invalid Date' || dateString === 'null' || dateString === 'undefined') {
|
|
24
|
+
return '';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Try to parse the date
|
|
28
|
+
const date = dayjs(dateString);
|
|
29
|
+
|
|
30
|
+
// Check if the parsed date is valid
|
|
31
|
+
if (!date.isValid()) {
|
|
32
|
+
// If it's already in DD/MM/YYYY format, return as is
|
|
33
|
+
if (typeof dateString === 'string' && /^\d{2}\/\d{2}\/\d{4}/.test(dateString)) {
|
|
34
|
+
return dateString;
|
|
35
|
+
}
|
|
36
|
+
// Return original value if we can't parse it
|
|
37
|
+
return dateString;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Format with time if the original string includes time
|
|
41
|
+
if (typeof dateString === 'string' && dateString.includes(':')) {
|
|
42
|
+
return date.format('DD/MM/YYYY hh:mm A');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Default to date only
|
|
46
|
+
return date.format('DD/MM/YYYY');
|
|
47
|
+
};
|
|
48
|
+
|
|
21
49
|
const renderProgressBar = (value, options = {}) => {
|
|
22
50
|
const percentage = parseFloat(value) || 0;
|
|
23
51
|
const { showPercentage = true, color = 'primary' } = options;
|
|
@@ -83,13 +111,27 @@ const renderStatusMessage = (status, messages) => {
|
|
|
83
111
|
padding: '4px 8px',
|
|
84
112
|
backgroundColor: '#f8f9fa',
|
|
85
113
|
borderRadius: '4px',
|
|
86
|
-
border: '1px solid #e9ecef'
|
|
114
|
+
border: '1px solid #e9ecef',
|
|
115
|
+
fontFamily: 'inherit'
|
|
87
116
|
}}>
|
|
88
117
|
{message}
|
|
89
118
|
</div>
|
|
90
119
|
);
|
|
91
120
|
};
|
|
92
121
|
|
|
122
|
+
const renderStatusText = (status) => {
|
|
123
|
+
// Simple text rendering without any special styling to avoid progress bar appearance
|
|
124
|
+
return (
|
|
125
|
+
<span style={{
|
|
126
|
+
fontFamily: 'inherit',
|
|
127
|
+
fontSize: '14px',
|
|
128
|
+
color: '#333'
|
|
129
|
+
}}>
|
|
130
|
+
{status || 'Unknown'}
|
|
131
|
+
</span>
|
|
132
|
+
);
|
|
133
|
+
};
|
|
134
|
+
|
|
93
135
|
const checkCondition = (rowData, condition) => {
|
|
94
136
|
if (!condition || !rowData) return false;
|
|
95
137
|
|
|
@@ -112,6 +154,106 @@ const renderCellContent = (value, column, widget, rowData = {}) => {
|
|
|
112
154
|
const columnFormatting = widget.columnFormatting || {};
|
|
113
155
|
const formatting = columnFormatting[column];
|
|
114
156
|
|
|
157
|
+
// Handle status column FIRST - render as plain text instead of progress bar
|
|
158
|
+
if (column === 'status' || column === 'processing_status' || column.toLowerCase().includes('status')) {
|
|
159
|
+
return renderStatusText(value);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Handle action buttons with icons and CSS tooltips
|
|
163
|
+
if (column === 'actions' && Array.isArray(value)) {
|
|
164
|
+
return (
|
|
165
|
+
<div style={{ display: 'flex', gap: '8px', justifyContent: 'center' }}>
|
|
166
|
+
{value.map((action, index) => {
|
|
167
|
+
const IconComponent = getActionIcon(action.action);
|
|
168
|
+
return (
|
|
169
|
+
<div
|
|
170
|
+
key={index}
|
|
171
|
+
className={`tooltip-container-${index}`}
|
|
172
|
+
style={{ position: 'relative', display: 'inline-block' }}
|
|
173
|
+
onMouseEnter={(e) => {
|
|
174
|
+
const tooltip = e.currentTarget.querySelector('.tooltip-text');
|
|
175
|
+
if (tooltip) {
|
|
176
|
+
tooltip.style.opacity = '1';
|
|
177
|
+
tooltip.style.visibility = 'visible';
|
|
178
|
+
}
|
|
179
|
+
const button = e.currentTarget.querySelector('button');
|
|
180
|
+
if (button) {
|
|
181
|
+
button.style.backgroundColor = '#f8f9fa';
|
|
182
|
+
button.style.transform = 'translateY(-1px)';
|
|
183
|
+
button.style.boxShadow = '0 2px 4px rgba(0,0,0,0.1)';
|
|
184
|
+
}
|
|
185
|
+
}}
|
|
186
|
+
onMouseLeave={(e) => {
|
|
187
|
+
const tooltip = e.currentTarget.querySelector('.tooltip-text');
|
|
188
|
+
if (tooltip) {
|
|
189
|
+
tooltip.style.opacity = '0';
|
|
190
|
+
tooltip.style.visibility = 'hidden';
|
|
191
|
+
}
|
|
192
|
+
const button = e.currentTarget.querySelector('button');
|
|
193
|
+
if (button) {
|
|
194
|
+
button.style.backgroundColor = '#fff';
|
|
195
|
+
button.style.transform = 'translateY(0)';
|
|
196
|
+
button.style.boxShadow = 'none';
|
|
197
|
+
}
|
|
198
|
+
}}
|
|
199
|
+
>
|
|
200
|
+
<button
|
|
201
|
+
onClick={() => handleActionClick(action, rowData)}
|
|
202
|
+
style={{
|
|
203
|
+
padding: '6px',
|
|
204
|
+
borderRadius: '4px',
|
|
205
|
+
border: '1px solid #dee2e6',
|
|
206
|
+
cursor: 'pointer',
|
|
207
|
+
backgroundColor: '#fff',
|
|
208
|
+
color: getActionIconColor(action.action),
|
|
209
|
+
display: 'flex',
|
|
210
|
+
alignItems: 'center',
|
|
211
|
+
justifyContent: 'center',
|
|
212
|
+
fontSize: '14px',
|
|
213
|
+
fontFamily: 'inherit',
|
|
214
|
+
transition: 'all 0.2s ease'
|
|
215
|
+
}}
|
|
216
|
+
>
|
|
217
|
+
<IconComponent size={14} />
|
|
218
|
+
</button>
|
|
219
|
+
<div className="tooltip-text" style={{
|
|
220
|
+
position: 'absolute',
|
|
221
|
+
bottom: '125%',
|
|
222
|
+
left: '50%',
|
|
223
|
+
transform: 'translateX(-50%)',
|
|
224
|
+
backgroundColor: '#333',
|
|
225
|
+
color: '#fff',
|
|
226
|
+
padding: '6px 8px',
|
|
227
|
+
borderRadius: '4px',
|
|
228
|
+
fontSize: '12px',
|
|
229
|
+
fontFamily: 'inherit',
|
|
230
|
+
whiteSpace: 'nowrap',
|
|
231
|
+
opacity: '0',
|
|
232
|
+
visibility: 'hidden',
|
|
233
|
+
transition: 'opacity 0.3s, visibility 0.3s',
|
|
234
|
+
zIndex: '1000',
|
|
235
|
+
pointerEvents: 'none'
|
|
236
|
+
}}>
|
|
237
|
+
{action.label}
|
|
238
|
+
<div style={{
|
|
239
|
+
position: 'absolute',
|
|
240
|
+
top: '100%',
|
|
241
|
+
left: '50%',
|
|
242
|
+
transform: 'translateX(-50%)',
|
|
243
|
+
width: 0,
|
|
244
|
+
height: 0,
|
|
245
|
+
borderLeft: '4px solid transparent',
|
|
246
|
+
borderRight: '4px solid transparent',
|
|
247
|
+
borderTop: '4px solid #333'
|
|
248
|
+
}} />
|
|
249
|
+
</div>
|
|
250
|
+
</div>
|
|
251
|
+
);
|
|
252
|
+
})}
|
|
253
|
+
</div>
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
|
|
115
257
|
if (formatting && formatting.type === 'progressBar') {
|
|
116
258
|
return renderProgressBar(value, formatting);
|
|
117
259
|
}
|
|
@@ -127,9 +269,280 @@ const renderCellContent = (value, column, widget, rowData = {}) => {
|
|
|
127
269
|
}
|
|
128
270
|
}
|
|
129
271
|
|
|
272
|
+
// Handle date columns - format for Australian display
|
|
273
|
+
const isDateColumn = column.toLowerCase().includes('date') ||
|
|
274
|
+
column.toLowerCase().includes('_at') ||
|
|
275
|
+
column === 'processed_at' ||
|
|
276
|
+
column === 'failed_at' ||
|
|
277
|
+
column === 'timestamp';
|
|
278
|
+
|
|
279
|
+
if (isDateColumn && value && value !== 'Invalid Date') {
|
|
280
|
+
const formattedDate = formatDateForDisplay(value);
|
|
281
|
+
return <span style={{ fontFamily: 'inherit' }}>{formattedDate}</span>;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Ensure consistent font family for all cell content
|
|
285
|
+
if (typeof value === 'string' || typeof value === 'number') {
|
|
286
|
+
return <span style={{ fontFamily: 'inherit' }}>{value}</span>;
|
|
287
|
+
}
|
|
288
|
+
|
|
130
289
|
return value;
|
|
131
290
|
};
|
|
132
291
|
|
|
292
|
+
const getActionButtonColor = (className) => {
|
|
293
|
+
if (className?.includes('btn-info')) return '#17a2b8';
|
|
294
|
+
if (className?.includes('btn-warning')) return '#ffc107';
|
|
295
|
+
if (className?.includes('btn-danger')) return '#dc3545';
|
|
296
|
+
if (className?.includes('btn-success')) return '#28a745';
|
|
297
|
+
return '#6c757d'; // default
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
const getActionIcon = (actionType) => {
|
|
301
|
+
switch (actionType) {
|
|
302
|
+
case 'viewLogs':
|
|
303
|
+
return Eye;
|
|
304
|
+
case 'retry':
|
|
305
|
+
return RotateCcw;
|
|
306
|
+
default:
|
|
307
|
+
return Eye; // default icon
|
|
308
|
+
}
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
const getActionIconColor = (actionType) => {
|
|
312
|
+
switch (actionType) {
|
|
313
|
+
case 'viewLogs':
|
|
314
|
+
return '#17a2b8'; // info color
|
|
315
|
+
case 'retry':
|
|
316
|
+
return '#ffc107'; // warning color
|
|
317
|
+
default:
|
|
318
|
+
return '#6c757d'; // default
|
|
319
|
+
}
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
const handleActionClick = (action, rowData) => {
|
|
323
|
+
switch (action.action) {
|
|
324
|
+
case 'viewLogs':
|
|
325
|
+
// Open logs in a modal
|
|
326
|
+
openLogsModal(action.url, rowData);
|
|
327
|
+
break;
|
|
328
|
+
case 'retry':
|
|
329
|
+
// Handle retry action
|
|
330
|
+
if (confirm('Are you sure you want to retry processing this document?')) {
|
|
331
|
+
fetch(action.url, {
|
|
332
|
+
method: action.method || 'POST',
|
|
333
|
+
headers: {
|
|
334
|
+
'Content-Type': 'application/json',
|
|
335
|
+
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')?.getAttribute('content')
|
|
336
|
+
}
|
|
337
|
+
})
|
|
338
|
+
.then(response => response.json())
|
|
339
|
+
.then(data => {
|
|
340
|
+
alert(data.message || 'Action completed');
|
|
341
|
+
// Optionally refresh the widget data
|
|
342
|
+
window.location.reload();
|
|
343
|
+
})
|
|
344
|
+
.catch(error => {
|
|
345
|
+
console.error('Action failed:', error);
|
|
346
|
+
alert('Action failed. Please try again.');
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
break;
|
|
350
|
+
default:
|
|
351
|
+
console.log('Unknown action:', action.action);
|
|
352
|
+
}
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
const openLogsModal = async (url, rowData) => {
|
|
356
|
+
try {
|
|
357
|
+
const response = await fetch(url, {
|
|
358
|
+
method: 'GET',
|
|
359
|
+
headers: {
|
|
360
|
+
'Content-Type': 'application/json',
|
|
361
|
+
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')?.getAttribute('content')
|
|
362
|
+
}
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
const data = await response.json();
|
|
366
|
+
|
|
367
|
+
// Create and open logs modal
|
|
368
|
+
const modalHtml = createLogsModalContent(data, rowData);
|
|
369
|
+
showModal('Document Processing Logs', modalHtml);
|
|
370
|
+
|
|
371
|
+
} catch (error) {
|
|
372
|
+
console.error('Failed to fetch logs:', error);
|
|
373
|
+
alert('Failed to load logs. Please try again.');
|
|
374
|
+
}
|
|
375
|
+
};
|
|
376
|
+
|
|
377
|
+
const createLogsModalContent = (data, rowData) => {
|
|
378
|
+
const { document, logs, summary } = data;
|
|
379
|
+
|
|
380
|
+
return `
|
|
381
|
+
<div style="max-height: 70vh; overflow-y: auto;">
|
|
382
|
+
<div style="background: #f8f9fa; padding: 15px; border-radius: 8px; margin-bottom: 20px;">
|
|
383
|
+
<h4 style="margin: 0 0 10px 0; color: #333;">Document Information</h4>
|
|
384
|
+
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 10px; font-size: 14px;">
|
|
385
|
+
<div><strong>File:</strong> ${document.filename}</div>
|
|
386
|
+
<div><strong>Status:</strong> <span style="color: ${getStatusColor(document.status)}">${document.status}</span></div>
|
|
387
|
+
<div><strong>Template:</strong> ${document.template}</div>
|
|
388
|
+
<div><strong>Processing Time:</strong> ${summary.processing_time}</div>
|
|
389
|
+
</div>
|
|
390
|
+
</div>
|
|
391
|
+
|
|
392
|
+
<div style="background: #fff3cd; padding: 10px; border-radius: 6px; margin-bottom: 15px; font-size: 14px;">
|
|
393
|
+
<strong>Summary:</strong> ${summary.total_logs} logs |
|
|
394
|
+
<span style="color: #dc3545;">${summary.errors} errors</span> |
|
|
395
|
+
<span style="color: #fd7e14;">${summary.warnings} warnings</span>
|
|
396
|
+
</div>
|
|
397
|
+
|
|
398
|
+
<div style="max-height: 400px; overflow-y: auto; border: 1px solid #dee2e6; border-radius: 6px;">
|
|
399
|
+
${logs.map(log => `
|
|
400
|
+
<div style="padding: 12px; border-bottom: 1px solid #eee; ${getLogBackgroundColor(log.level)}">
|
|
401
|
+
<div style="display: flex; justify-content: between; align-items: flex-start; margin-bottom: 8px;">
|
|
402
|
+
<div style="flex: 1;">
|
|
403
|
+
<span style="background: ${getLogLevelColor(log.level)}; color: white; padding: 2px 8px; border-radius: 12px; font-size: 11px; text-transform: uppercase;">${log.level}</span>
|
|
404
|
+
<span style="color: #6c757d; font-size: 12px; margin-left: 10px;">${log.stage}</span>
|
|
405
|
+
</div>
|
|
406
|
+
<div style="font-size: 11px; color: #6c757d;">${log.timestamp}</div>
|
|
407
|
+
</div>
|
|
408
|
+
<div style="font-size: 14px; margin-bottom: 5px;">${log.message}</div>
|
|
409
|
+
${log.execution_time_ms ? `<div style="font-size: 12px; color: #6c757d; display: flex; align-items: center; gap: 8px;">
|
|
410
|
+
<span style="display: inline-flex; align-items: center; gap: 4px;">
|
|
411
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
412
|
+
<circle cx="12" cy="12" r="10"/>
|
|
413
|
+
<polyline points="12,6 12,12 16,14"/>
|
|
414
|
+
</svg>
|
|
415
|
+
${log.execution_time_ms}ms
|
|
416
|
+
</span>
|
|
417
|
+
<span style="display: inline-flex; align-items: center; gap: 4px;">
|
|
418
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
419
|
+
<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/>
|
|
420
|
+
<polyline points="3.29,7 12,12 20.71,7"/>
|
|
421
|
+
<line x1="12" y1="22" x2="12" y2="12"/>
|
|
422
|
+
</svg>
|
|
423
|
+
${log.memory_usage_mb}MB
|
|
424
|
+
</span>
|
|
425
|
+
</div>` : ''}
|
|
426
|
+
${log.details && Object.keys(log.details).length > 0 ? `
|
|
427
|
+
<details style="margin-top: 8px;">
|
|
428
|
+
<summary style="font-size: 12px; color: #007bff; cursor: pointer;">View Details</summary>
|
|
429
|
+
<pre style="background: #f8f9fa; padding: 8px; border-radius: 4px; font-size: 11px; margin-top: 5px; white-space: pre-wrap;">${JSON.stringify(log.details, null, 2)}</pre>
|
|
430
|
+
</details>
|
|
431
|
+
` : ''}
|
|
432
|
+
${log.stack_trace ? `
|
|
433
|
+
<details style="margin-top: 8px;">
|
|
434
|
+
<summary style="font-size: 12px; color: #dc3545; cursor: pointer;">Stack Trace</summary>
|
|
435
|
+
<pre style="background: #f8d7da; padding: 8px; border-radius: 4px; font-size: 11px; margin-top: 5px; white-space: pre-wrap;">${log.stack_trace}</pre>
|
|
436
|
+
</details>
|
|
437
|
+
` : ''}
|
|
438
|
+
</div>
|
|
439
|
+
`).join('')}
|
|
440
|
+
</div>
|
|
441
|
+
</div>
|
|
442
|
+
`;
|
|
443
|
+
};
|
|
444
|
+
|
|
445
|
+
const getStatusColor = (status) => {
|
|
446
|
+
switch (status) {
|
|
447
|
+
case 'completed': return '#28a745';
|
|
448
|
+
case 'failed': return '#dc3545';
|
|
449
|
+
case 'processing': return '#007bff';
|
|
450
|
+
case 'pending': return '#6c757d';
|
|
451
|
+
default: return '#6c757d';
|
|
452
|
+
}
|
|
453
|
+
};
|
|
454
|
+
|
|
455
|
+
const getLogLevelColor = (level) => {
|
|
456
|
+
switch (level.toLowerCase()) {
|
|
457
|
+
case 'error': return '#dc3545';
|
|
458
|
+
case 'warning': return '#fd7e14';
|
|
459
|
+
case 'info': return '#007bff';
|
|
460
|
+
case 'debug': return '#6c757d';
|
|
461
|
+
default: return '#6c757d';
|
|
462
|
+
}
|
|
463
|
+
};
|
|
464
|
+
|
|
465
|
+
const getLogBackgroundColor = (level) => {
|
|
466
|
+
switch (level.toLowerCase()) {
|
|
467
|
+
case 'error': return 'background-color: #f8d7da;';
|
|
468
|
+
case 'warning': return 'background-color: #fff3cd;';
|
|
469
|
+
default: return '';
|
|
470
|
+
}
|
|
471
|
+
};
|
|
472
|
+
|
|
473
|
+
const showModal = (title, content) => {
|
|
474
|
+
// Remove any existing modal
|
|
475
|
+
const existingModal = document.getElementById('logs-modal');
|
|
476
|
+
if (existingModal) {
|
|
477
|
+
existingModal.remove();
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// Create modal HTML
|
|
481
|
+
const modalHtml = `
|
|
482
|
+
<div id="logs-modal" style="
|
|
483
|
+
position: fixed;
|
|
484
|
+
top: 0;
|
|
485
|
+
left: 0;
|
|
486
|
+
width: 100%;
|
|
487
|
+
height: 100%;
|
|
488
|
+
background: rgba(0,0,0,0.5);
|
|
489
|
+
z-index: 10000;
|
|
490
|
+
display: flex;
|
|
491
|
+
align-items: center;
|
|
492
|
+
justify-content: center;
|
|
493
|
+
">
|
|
494
|
+
<div style="
|
|
495
|
+
background: white;
|
|
496
|
+
border-radius: 8px;
|
|
497
|
+
max-width: 90vw;
|
|
498
|
+
max-height: 90vh;
|
|
499
|
+
width: 1000px;
|
|
500
|
+
box-shadow: 0 10px 30px rgba(0,0,0,0.3);
|
|
501
|
+
display: flex;
|
|
502
|
+
flex-direction: column;
|
|
503
|
+
">
|
|
504
|
+
<div style="
|
|
505
|
+
padding: 20px;
|
|
506
|
+
border-bottom: 1px solid #dee2e6;
|
|
507
|
+
display: flex;
|
|
508
|
+
justify-content: space-between;
|
|
509
|
+
align-items: center;
|
|
510
|
+
flex-shrink: 0;
|
|
511
|
+
">
|
|
512
|
+
<h2 style="margin: 0; color: #333; font-size: 18px;">${title}</h2>
|
|
513
|
+
<button onclick="document.getElementById('logs-modal').remove()" style="
|
|
514
|
+
background: none;
|
|
515
|
+
border: none;
|
|
516
|
+
font-size: 24px;
|
|
517
|
+
cursor: pointer;
|
|
518
|
+
color: #6c757d;
|
|
519
|
+
padding: 0;
|
|
520
|
+
width: 30px;
|
|
521
|
+
height: 30px;
|
|
522
|
+
display: flex;
|
|
523
|
+
align-items: center;
|
|
524
|
+
justify-content: center;
|
|
525
|
+
">×</button>
|
|
526
|
+
</div>
|
|
527
|
+
<div style="padding: 20px; overflow: hidden; flex: 1;">
|
|
528
|
+
${content}
|
|
529
|
+
</div>
|
|
530
|
+
</div>
|
|
531
|
+
</div>
|
|
532
|
+
`;
|
|
533
|
+
|
|
534
|
+
// Add modal to page
|
|
535
|
+
document.body.insertAdjacentHTML('beforeend', modalHtml);
|
|
536
|
+
|
|
537
|
+
// Close modal when clicking outside
|
|
538
|
+
document.getElementById('logs-modal').addEventListener('click', (e) => {
|
|
539
|
+
if (e.target.id === 'logs-modal') {
|
|
540
|
+
e.target.remove();
|
|
541
|
+
}
|
|
542
|
+
});
|
|
543
|
+
};
|
|
544
|
+
|
|
545
|
+
|
|
133
546
|
function GenericDashboard({ setting, userProfile, dynamicDashboard = false }) {
|
|
134
547
|
const swapy = useRef(null);
|
|
135
548
|
const container = useRef(null);
|
|
@@ -141,8 +554,6 @@ function GenericDashboard({ setting, userProfile, dynamicDashboard = false }) {
|
|
|
141
554
|
const [data, setData] = useState({});
|
|
142
555
|
const [dropdowns, setDropdowns] = useState({});
|
|
143
556
|
const [filters, setFilters] = useState({});
|
|
144
|
-
const [modalShow, setModalShow] = useState(false);
|
|
145
|
-
const [modalContent, setModalContent] = useState(null);
|
|
146
557
|
const [editMode, setEditMode] = useState(false);
|
|
147
558
|
const [showAddWidgetModal, setShowAddWidgetModal] = useState(false);
|
|
148
559
|
const [selectedAddWidgetId, setSelectedAddWidgetId] = useState('');
|
|
@@ -165,15 +576,15 @@ function GenericDashboard({ setting, userProfile, dynamicDashboard = false }) {
|
|
|
165
576
|
const getDefaultDate = (defaultValue) => {
|
|
166
577
|
switch (defaultValue) {
|
|
167
578
|
case 'startOfMonth':
|
|
168
|
-
return dayjs().startOf('month').format('YYYY-MM-DD');
|
|
579
|
+
return dayjs().startOf('month').format('YYYY-MM-DD'); // Keep YYYY-MM-DD for HTML date inputs
|
|
169
580
|
case 'endOfMonth':
|
|
170
|
-
return dayjs().endOf('month').format('YYYY-MM-DD');
|
|
581
|
+
return dayjs().endOf('month').format('YYYY-MM-DD'); // Keep YYYY-MM-DD for HTML date inputs
|
|
171
582
|
default:
|
|
172
583
|
return '';
|
|
173
584
|
}
|
|
174
585
|
};
|
|
175
586
|
|
|
176
|
-
// Initialize filters based on each widget
|
|
587
|
+
// Initialize filters based on each widget's filters.
|
|
177
588
|
useEffect(() => {
|
|
178
589
|
const initialFilters = {};
|
|
179
590
|
dashboardSetting.widgets.forEach((widget) => {
|
|
@@ -352,14 +763,6 @@ function GenericDashboard({ setting, userProfile, dynamicDashboard = false }) {
|
|
|
352
763
|
}
|
|
353
764
|
}, [dashboardSetting]);
|
|
354
765
|
|
|
355
|
-
const openModal = (widgetId) => {
|
|
356
|
-
setModalContent(widgetId);
|
|
357
|
-
setModalShow(true);
|
|
358
|
-
};
|
|
359
|
-
|
|
360
|
-
const closeModal = () => {
|
|
361
|
-
setModalShow(false);
|
|
362
|
-
};
|
|
363
766
|
|
|
364
767
|
const handleFilterChange = (widgetId, key, value) => {
|
|
365
768
|
setFilters((prevFilters) => ({
|
|
@@ -1252,6 +1655,7 @@ function GenericDashboard({ setting, userProfile, dynamicDashboard = false }) {
|
|
|
1252
1655
|
) => (
|
|
1253
1656
|
<td
|
|
1254
1657
|
key={`table-data-${rowIndex}-${columnIndex}`}
|
|
1658
|
+
style={{ fontFamily: 'inherit' }}
|
|
1255
1659
|
>
|
|
1256
1660
|
{renderCellContent(
|
|
1257
1661
|
row[column],
|
|
@@ -1493,17 +1897,6 @@ function GenericDashboard({ setting, userProfile, dynamicDashboard = false }) {
|
|
|
1493
1897
|
</div>
|
|
1494
1898
|
{renderFilters(widget)}
|
|
1495
1899
|
<div>{renderWidget(widget)}</div>
|
|
1496
|
-
{widget.modal && (
|
|
1497
|
-
<div className={styles.widgetAction}>
|
|
1498
|
-
<button
|
|
1499
|
-
className={`${styles.btn} ${styles.dmore}`}
|
|
1500
|
-
onClick={() => openModal(widget.id)}
|
|
1501
|
-
>
|
|
1502
|
-
View all{' '}
|
|
1503
|
-
<span>({data[widget.id]?.length || 0})</span>
|
|
1504
|
-
</button>
|
|
1505
|
-
</div>
|
|
1506
|
-
)}
|
|
1507
1900
|
{editMode && (
|
|
1508
1901
|
<div className={styles.widgetTools}>
|
|
1509
1902
|
<div className={styles.resizeTools}>
|
|
@@ -1568,35 +1961,6 @@ function GenericDashboard({ setting, userProfile, dynamicDashboard = false }) {
|
|
|
1568
1961
|
)}
|
|
1569
1962
|
<div className={styles.grid} ref={container}>
|
|
1570
1963
|
{renderWidgets()}
|
|
1571
|
-
<Popup open={modalShow} onClose={closeModal} closeOnDocumentClick={false}>
|
|
1572
|
-
<div className="modalwrap top--modal modalWide">
|
|
1573
|
-
<div className="modal">
|
|
1574
|
-
<div className="modal__header">
|
|
1575
|
-
<h1>
|
|
1576
|
-
{
|
|
1577
|
-
dashboardSetting.modalTitles?.[
|
|
1578
|
-
modalContent
|
|
1579
|
-
]
|
|
1580
|
-
}
|
|
1581
|
-
</h1>
|
|
1582
|
-
<button
|
|
1583
|
-
className="modal__close"
|
|
1584
|
-
onClick={closeModal}
|
|
1585
|
-
>
|
|
1586
|
-
<X strokeWidth={1} size={24} />
|
|
1587
|
-
</button>
|
|
1588
|
-
</div>
|
|
1589
|
-
<div className="modal__content">
|
|
1590
|
-
{modalContent &&
|
|
1591
|
-
renderWidget(
|
|
1592
|
-
dashboardSetting.widgets.find(
|
|
1593
|
-
(w) => w.id === modalContent
|
|
1594
|
-
)
|
|
1595
|
-
)}
|
|
1596
|
-
</div>
|
|
1597
|
-
</div>
|
|
1598
|
-
</div>
|
|
1599
|
-
</Popup>
|
|
1600
1964
|
<Popup
|
|
1601
1965
|
open={showAddWidgetModal}
|
|
1602
1966
|
onClose={() => setShowAddWidgetModal(false)}
|
|
@@ -64,6 +64,7 @@ import {
|
|
|
64
64
|
X as CircleX,
|
|
65
65
|
} from 'lucide-react';
|
|
66
66
|
import { confirmDialog } from '../utils/ConfirmDialog';
|
|
67
|
+
import OcrTemplateEnhanced from './OcrTemplateEnhanced';
|
|
67
68
|
|
|
68
69
|
import 'react-big-calendar/lib/css/react-big-calendar.css';
|
|
69
70
|
import 'react-big-calendar/lib/addons/dragAndDrop/styles.css';
|
|
@@ -1435,7 +1436,16 @@ function GenericDetail({
|
|
|
1435
1436
|
case 'link':
|
|
1436
1437
|
return renderLink();
|
|
1437
1438
|
case 'ocrTemplate':
|
|
1438
|
-
return
|
|
1439
|
+
return (
|
|
1440
|
+
<OcrTemplateEnhanced
|
|
1441
|
+
id={id}
|
|
1442
|
+
data={data}
|
|
1443
|
+
setData={setData}
|
|
1444
|
+
routeParams={routeParams}
|
|
1445
|
+
urlParam={urlParam}
|
|
1446
|
+
onSave={handleOcrTemplateSave}
|
|
1447
|
+
/>
|
|
1448
|
+
);
|
|
1439
1449
|
case 'options':
|
|
1440
1450
|
return renderOptions();
|
|
1441
1451
|
case 'password':
|
|
@@ -2613,15 +2623,19 @@ function GenericDetail({
|
|
|
2613
2623
|
className={
|
|
2614
2624
|
item.size ===
|
|
2615
2625
|
'full'
|
|
2616
|
-
?
|
|
2626
|
+
? item.type === 'ocrTemplate'
|
|
2627
|
+
? styles['fw-grid-item']
|
|
2628
|
+
: `${styles['fw-grid-item']} ${styles.notecolor}`
|
|
2617
2629
|
: null
|
|
2618
2630
|
}
|
|
2619
2631
|
>
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2632
|
+
{item.type !== 'ocrTemplate' && (
|
|
2633
|
+
<strong>
|
|
2634
|
+
{item.label
|
|
2635
|
+
? `${item.label}:`
|
|
2636
|
+
: null}
|
|
2637
|
+
</strong>
|
|
2638
|
+
)}{item.type !== 'ocrTemplate' ? ' ' : ''}
|
|
2625
2639
|
{item
|
|
2626
2640
|
.href
|
|
2627
2641
|
?.url &&
|