@visns-studio/visns-components 5.14.18 → 5.14.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/README.md +175 -0
- package/package.json +2 -2
- package/src/components/Autocomplete.jsx +1 -1
- package/src/components/Field.jsx +395 -1
- package/src/components/Form.jsx +314 -0
- package/src/components/styles/Field.module.scss +258 -0
package/README.md
CHANGED
|
@@ -9,6 +9,18 @@ A comprehensive React component library used by the VISNS Studio team for CRM an
|
|
|
9
9
|
|
|
10
10
|
VISNS Components is a React-based UI component library that provides a set of reusable, consistent, and customizable components for building web applications. It includes components for authentication, data grids, forms, navigation, and more, designed to work seamlessly together.
|
|
11
11
|
|
|
12
|
+
## Recent Updates (v5.14.20)
|
|
13
|
+
|
|
14
|
+
### Latest Enhancements
|
|
15
|
+
|
|
16
|
+
- **GenericGrid Advanced Features**: Enhanced filtering with URL-based options, multiselect support, contact display integration, interactive date pickers with timezone support, and toggleable stage functionality
|
|
17
|
+
- **Modular Architecture**: GenericIndex refactored into standalone GenericGrid and GenericReportForm components for better reusability
|
|
18
|
+
- **Proposal System Integration**: GenericQuote enhanced with comprehensive proposal generation capabilities
|
|
19
|
+
- **Smart Data Extraction**: DropZone component with intelligent data extraction for complex API responses
|
|
20
|
+
- **Interactive Stage Management**: Clickable stage toggles with API integration and visual feedback
|
|
21
|
+
- **Timezone Support**: Automatic timezone detection and conversion for date operations
|
|
22
|
+
- **Contact Selection Workflows**: Two-step date and contact selection for survey systems
|
|
23
|
+
|
|
12
24
|
## Features
|
|
13
25
|
|
|
14
26
|
### Authentication System
|
|
@@ -180,6 +192,169 @@ The refactoring also introduced shared utility functions:
|
|
|
180
192
|
|
|
181
193
|
These utilities are available for use in custom components and provide consistent functionality across the library.
|
|
182
194
|
|
|
195
|
+
### GenericGrid Advanced Features
|
|
196
|
+
|
|
197
|
+
#### Filter Enhancements (v5.10.12+)
|
|
198
|
+
|
|
199
|
+
The GenericGrid component includes powerful filtering capabilities with URL-based options and multiselect support:
|
|
200
|
+
|
|
201
|
+
```jsx
|
|
202
|
+
// URL-based filter options
|
|
203
|
+
{
|
|
204
|
+
id: 'status',
|
|
205
|
+
type: 'dropdown',
|
|
206
|
+
label: 'Status',
|
|
207
|
+
optionsUrl: '/api/filter-options/status',
|
|
208
|
+
optionsMethod: 'POST',
|
|
209
|
+
isMulti: true // Enables multiple selection
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Year filter with "now" default value
|
|
213
|
+
{
|
|
214
|
+
id: 'year',
|
|
215
|
+
type: 'year',
|
|
216
|
+
label: 'Year',
|
|
217
|
+
defaultValue: 'now' // Displays current year
|
|
218
|
+
}
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
**Key Features:**
|
|
222
|
+
- **Dynamic Options**: Filters can fetch options from API endpoints using POST method
|
|
223
|
+
- **Multiselect Control**: Use `isMulti` property for single/multiple selection control
|
|
224
|
+
- **Smart Defaults**: Year filters properly handle "now" value to display current year
|
|
225
|
+
- **Value Initialization**: Enhanced value formatting for MultiSelect components
|
|
226
|
+
|
|
227
|
+
#### Contact Display Integration (v5.11.1+)
|
|
228
|
+
|
|
229
|
+
GenericGrid automatically displays associated contacts when date fields have contact relationships:
|
|
230
|
+
|
|
231
|
+
```jsx
|
|
232
|
+
// Date column with contact display
|
|
233
|
+
{
|
|
234
|
+
id: 'survey_date',
|
|
235
|
+
type: 'date',
|
|
236
|
+
header: 'Survey Date',
|
|
237
|
+
onClick: {
|
|
238
|
+
type: 'date_with_contacts',
|
|
239
|
+
title: 'Mark Survey Complete',
|
|
240
|
+
message: 'Select the survey completion date'
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
**Features:**
|
|
246
|
+
- **Date + Contact Display**: Shows formatted date with contact summary badge
|
|
247
|
+
- **Contact Tooltips**: Hover over contact badge for full contact details
|
|
248
|
+
- **Flexible Data Structure**: Supports both existing contacts and manual entries
|
|
249
|
+
- **Responsive Design**: Contact badges adapt to different screen sizes
|
|
250
|
+
|
|
251
|
+
#### Interactive Date Picker (v5.10.13+)
|
|
252
|
+
|
|
253
|
+
Enhanced date cell interaction with confirmation dialogs and timezone support:
|
|
254
|
+
|
|
255
|
+
```jsx
|
|
256
|
+
// Date picker with timezone support
|
|
257
|
+
{
|
|
258
|
+
id: 'completion_date',
|
|
259
|
+
type: 'date',
|
|
260
|
+
onClick: {
|
|
261
|
+
type: 'date_picker',
|
|
262
|
+
title: 'Set Completion Date',
|
|
263
|
+
message: 'Choose the completion date',
|
|
264
|
+
placeholder: 'Select date',
|
|
265
|
+
timezone: 'auto', // or 'Australia/Perth'
|
|
266
|
+
dateOnly: true // For DATE fields vs DATETIME
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
**Timezone Features (v5.10.15+):**
|
|
272
|
+
- **Automatic Detection**: `timezone: "auto"` detects browser timezone
|
|
273
|
+
- **Australia/Perth Support**: Built-in support with `forcePerth: true`
|
|
274
|
+
- **Custom Timezones**: Support any timezone with `timezone: "specific/timezone"`
|
|
275
|
+
- **UTC Conversion**: Proper conversion between local timezone and UTC for API calls
|
|
276
|
+
- **Field Type Awareness**: `dateOnly` parameter handles DATE vs DATETIME fields correctly
|
|
277
|
+
|
|
278
|
+
#### Contact Selection Workflow (v5.11.0+)
|
|
279
|
+
|
|
280
|
+
Two-step date and contact selection process for comprehensive data collection:
|
|
281
|
+
|
|
282
|
+
```jsx
|
|
283
|
+
// Date + contact selection configuration
|
|
284
|
+
{
|
|
285
|
+
id: 'survey_date',
|
|
286
|
+
type: 'date',
|
|
287
|
+
onClick: {
|
|
288
|
+
type: 'date_with_contacts',
|
|
289
|
+
contactConfig: {
|
|
290
|
+
maxContacts: 5,
|
|
291
|
+
allowManual: true,
|
|
292
|
+
contactsEndpoint: '/api/contacts/search',
|
|
293
|
+
requiredFields: ['name', 'email']
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
**Contact Selection Features:**
|
|
300
|
+
- **ContactSelectorModal**: Modal for selecting existing or adding manual contacts
|
|
301
|
+
- **Multiple Contact Types**: Database contacts (`type: 'contact'`) and manual entries (`type: 'manual'`)
|
|
302
|
+
- **Flexible Limits**: Configurable contact limits and required fields
|
|
303
|
+
- **Survey Integration**: Perfect for survey systems requiring contact associations
|
|
304
|
+
|
|
305
|
+
#### Toggleable Stage Functionality (v5.12.2+)
|
|
306
|
+
|
|
307
|
+
Interactive stage management with toggle functionality for both stage and stageCounter columns:
|
|
308
|
+
|
|
309
|
+
```jsx
|
|
310
|
+
// Basic stage column with toggle
|
|
311
|
+
{
|
|
312
|
+
id: 'opportunity_stage',
|
|
313
|
+
type: 'stage',
|
|
314
|
+
keyIds: ['prospect', 'qualified', 'proposal', 'won'],
|
|
315
|
+
valueIds: ['Prospect', 'Qualified', 'Proposal', 'Won'],
|
|
316
|
+
toggleable: true,
|
|
317
|
+
toggleConfig: {
|
|
318
|
+
updateUrl: '/api/opportunities/{id}/stage',
|
|
319
|
+
updateMethod: 'PATCH',
|
|
320
|
+
updateKey: 'stage_field'
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// StageCounter with individual stage toggles
|
|
325
|
+
{
|
|
326
|
+
id: 'project_stages',
|
|
327
|
+
type: 'stageCounter',
|
|
328
|
+
stageConfig: {
|
|
329
|
+
key: 'milestones',
|
|
330
|
+
stageLabelKey: 'milestone_name',
|
|
331
|
+
stageColour: {
|
|
332
|
+
key: 'status',
|
|
333
|
+
options: [
|
|
334
|
+
{ id: 'completed', colour: '#28a745' },
|
|
335
|
+
{ id: 'pending', colour: '#6c757d' }
|
|
336
|
+
]
|
|
337
|
+
},
|
|
338
|
+
toggleable: true,
|
|
339
|
+
toggleConfig: {
|
|
340
|
+
activeStatusValue: 'completed',
|
|
341
|
+
inactiveStatusValue: 'pending',
|
|
342
|
+
statusField: 'status',
|
|
343
|
+
updateUrl: '/api/project-stages/{stage_id}',
|
|
344
|
+
updateMethod: 'PATCH',
|
|
345
|
+
updateKey: 'status'
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
**Toggle Features:**
|
|
352
|
+
- **Interactive Management**: Click stages to enable/disable with visual feedback
|
|
353
|
+
- **API Integration**: Configurable endpoints with URL placeholders and CSRF protection
|
|
354
|
+
- **Toast Notifications**: Real-time success/error feedback
|
|
355
|
+
- **Auto-Refresh**: Grid data refreshes after successful stage updates
|
|
356
|
+
- **Mixed Interactions**: StageCounter supports both stage toggling and container navigation
|
|
357
|
+
|
|
183
358
|
## DropZone Component Enhancements (v5.14.0+)
|
|
184
359
|
|
|
185
360
|
### Smart Data Extraction
|
package/package.json
CHANGED
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"html-react-parser": "^5.2.6",
|
|
29
29
|
"lodash": "^4.17.21",
|
|
30
30
|
"lodash.debounce": "^4.0.8",
|
|
31
|
-
"lucide-react": "^0.
|
|
31
|
+
"lucide-react": "^0.536.0",
|
|
32
32
|
"moment": "^2.30.1",
|
|
33
33
|
"motion": "^12.23.12",
|
|
34
34
|
"numeral": "^2.0.6",
|
|
@@ -89,7 +89,7 @@
|
|
|
89
89
|
"react-dom": "^17.0.0 || ^18.0.0"
|
|
90
90
|
},
|
|
91
91
|
"name": "@visns-studio/visns-components",
|
|
92
|
-
"version": "5.14.
|
|
92
|
+
"version": "5.14.20",
|
|
93
93
|
"description": "Various packages to assist in the development of our Custom Applications.",
|
|
94
94
|
"main": "src/index.js",
|
|
95
95
|
"files": [
|
|
@@ -274,7 +274,7 @@ const VisnsAutocomplete = memo((props) => {
|
|
|
274
274
|
)}
|
|
275
275
|
</div>
|
|
276
276
|
|
|
277
|
-
{isFocused && !overwrite && (search.length > 1 || isLoading) && (
|
|
277
|
+
{isFocused && !overwrite && (search && search.length > 1 || isLoading) && (
|
|
278
278
|
<ul
|
|
279
279
|
ref={resultsRef}
|
|
280
280
|
className={styles['AutocompletePlace-results']}
|
package/src/components/Field.jsx
CHANGED
|
@@ -65,6 +65,9 @@ function Field({
|
|
|
65
65
|
onChangeTableRadio,
|
|
66
66
|
onChangeAddTableTextRow,
|
|
67
67
|
onChangeTableText,
|
|
68
|
+
onChangeJsonTable,
|
|
69
|
+
onAddJsonTableRow,
|
|
70
|
+
onDeleteJsonTableRow,
|
|
68
71
|
onChangeToggle,
|
|
69
72
|
onFileDownload,
|
|
70
73
|
setFormData,
|
|
@@ -1409,6 +1412,396 @@ function Field({
|
|
|
1409
1412
|
autoComplete="off"
|
|
1410
1413
|
/>
|
|
1411
1414
|
);
|
|
1415
|
+
case 'json-table':
|
|
1416
|
+
// Parse JSON value to get table data
|
|
1417
|
+
let jsonTableData = [];
|
|
1418
|
+
let jsonTableError = null;
|
|
1419
|
+
|
|
1420
|
+
try {
|
|
1421
|
+
if (inputValue && inputValue !== 'null' && inputValue !== '') {
|
|
1422
|
+
const parsedValue = typeof inputValue === 'string' ? JSON.parse(inputValue) : inputValue;
|
|
1423
|
+
|
|
1424
|
+
// Handle jsonPath setting to extract nested data
|
|
1425
|
+
if (settings.jsonPath && parsedValue[settings.jsonPath]) {
|
|
1426
|
+
jsonTableData = Array.isArray(parsedValue[settings.jsonPath])
|
|
1427
|
+
? parsedValue[settings.jsonPath]
|
|
1428
|
+
: [];
|
|
1429
|
+
} else if (Array.isArray(parsedValue)) {
|
|
1430
|
+
jsonTableData = parsedValue;
|
|
1431
|
+
} else if (typeof parsedValue === 'object' && parsedValue !== null) {
|
|
1432
|
+
// If it's an object but not an array, convert to single row
|
|
1433
|
+
jsonTableData = [parsedValue];
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1436
|
+
} catch (e) {
|
|
1437
|
+
jsonTableError = 'Invalid JSON format';
|
|
1438
|
+
jsonTableData = [];
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
// Ensure minimum rows
|
|
1442
|
+
const minRows = settings.validation?.minRows || settings.minRows || 0;
|
|
1443
|
+
while (jsonTableData.length < minRows) {
|
|
1444
|
+
jsonTableData.push(settings.defaultRow || {});
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
// Custom validation for JSON table
|
|
1448
|
+
const validateJsonTable = (data, validationConfig) => {
|
|
1449
|
+
if (!validationConfig || !validationConfig.customValidation) {
|
|
1450
|
+
return { isValid: true, errors: [] };
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
const { type, startTimeField, endTimeField, message } = validationConfig.customValidation;
|
|
1454
|
+
const errors = [];
|
|
1455
|
+
|
|
1456
|
+
if (type === '24hour_coverage') {
|
|
1457
|
+
// Convert time strings to minutes for easier calculation
|
|
1458
|
+
const timeToMinutes = (timeStr) => {
|
|
1459
|
+
if (!timeStr) return null;
|
|
1460
|
+
const [hours, minutes] = timeStr.split(':').map(Number);
|
|
1461
|
+
return hours * 60 + minutes;
|
|
1462
|
+
};
|
|
1463
|
+
|
|
1464
|
+
// Sort intervals by start time
|
|
1465
|
+
const sortedIntervals = [...data].sort((a, b) => {
|
|
1466
|
+
const aStart = timeToMinutes(a[startTimeField]);
|
|
1467
|
+
const bStart = timeToMinutes(b[startTimeField]);
|
|
1468
|
+
return aStart - bStart;
|
|
1469
|
+
});
|
|
1470
|
+
|
|
1471
|
+
let totalCoverage = 0;
|
|
1472
|
+
let previousEnd = null;
|
|
1473
|
+
|
|
1474
|
+
for (let i = 0; i < sortedIntervals.length; i++) {
|
|
1475
|
+
const interval = sortedIntervals[i];
|
|
1476
|
+
const startMinutes = timeToMinutes(interval[startTimeField]);
|
|
1477
|
+
const endMinutes = timeToMinutes(interval[endTimeField]);
|
|
1478
|
+
|
|
1479
|
+
if (startMinutes === null || endMinutes === null) {
|
|
1480
|
+
errors.push(`Interval ${i + 1}: Missing start or end time`);
|
|
1481
|
+
continue;
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
// Handle cross-midnight intervals (e.g., 18:00 to 06:00)
|
|
1485
|
+
let duration;
|
|
1486
|
+
if (endMinutes <= startMinutes) {
|
|
1487
|
+
// Cross-midnight: add 24 hours to end time
|
|
1488
|
+
duration = (endMinutes + 1440) - startMinutes;
|
|
1489
|
+
} else {
|
|
1490
|
+
duration = endMinutes - startMinutes;
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
totalCoverage += duration;
|
|
1494
|
+
|
|
1495
|
+
// Check for gaps (except for the first interval)
|
|
1496
|
+
if (previousEnd !== null && startMinutes !== previousEnd) {
|
|
1497
|
+
// Allow for cross-midnight transitions
|
|
1498
|
+
const gapSize = startMinutes > previousEnd
|
|
1499
|
+
? startMinutes - previousEnd
|
|
1500
|
+
: (startMinutes + 1440) - previousEnd;
|
|
1501
|
+
|
|
1502
|
+
if (gapSize > 0 && gapSize < 1440) { // Don't count full day gaps as errors
|
|
1503
|
+
errors.push(`Gap detected between intervals: ${Math.floor(gapSize / 60)}h ${gapSize % 60}m`);
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
// Update previousEnd, handling cross-midnight
|
|
1508
|
+
previousEnd = endMinutes <= startMinutes ? endMinutes + 1440 : endMinutes;
|
|
1509
|
+
if (previousEnd >= 1440) previousEnd -= 1440; // Normalize back to 0-1439
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
// Check if total coverage equals 24 hours (1440 minutes)
|
|
1513
|
+
if (Math.abs(totalCoverage - 1440) > 1) { // Allow 1 minute tolerance
|
|
1514
|
+
errors.push(`Total coverage is ${Math.floor(totalCoverage / 60)}h ${totalCoverage % 60}m. Required: 24h 0m`);
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1517
|
+
// Check for overlaps
|
|
1518
|
+
for (let i = 0; i < sortedIntervals.length - 1; i++) {
|
|
1519
|
+
const current = sortedIntervals[i];
|
|
1520
|
+
const next = sortedIntervals[i + 1];
|
|
1521
|
+
|
|
1522
|
+
const currentStart = timeToMinutes(current[startTimeField]);
|
|
1523
|
+
const currentEnd = timeToMinutes(current[endTimeField]);
|
|
1524
|
+
const nextStart = timeToMinutes(next[startTimeField]);
|
|
1525
|
+
|
|
1526
|
+
// Handle normal and cross-midnight intervals
|
|
1527
|
+
const currentActualEnd = currentEnd <= currentStart ? currentEnd + 1440 : currentEnd;
|
|
1528
|
+
const nextActualStart = nextStart;
|
|
1529
|
+
|
|
1530
|
+
if (currentActualEnd > nextActualStart && currentActualEnd < nextActualStart + 1440) {
|
|
1531
|
+
const overlapMinutes = currentActualEnd - nextActualStart;
|
|
1532
|
+
errors.push(`Overlap detected: ${Math.floor(overlapMinutes / 60)}h ${overlapMinutes % 60}m between intervals ${i + 1} and ${i + 2}`);
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
return {
|
|
1538
|
+
isValid: errors.length === 0,
|
|
1539
|
+
errors: errors.length > 0 ? [message, ...errors] : []
|
|
1540
|
+
};
|
|
1541
|
+
};
|
|
1542
|
+
|
|
1543
|
+
// Perform validation
|
|
1544
|
+
const validationResult = validateJsonTable(jsonTableData, settings.validation);
|
|
1545
|
+
const hasValidationErrors = !validationResult.isValid;
|
|
1546
|
+
|
|
1547
|
+
// Render JSON table field
|
|
1548
|
+
const renderJsonTableField = (column, cellValue, rowIndex, columnIndex) => {
|
|
1549
|
+
const fieldSettings = {
|
|
1550
|
+
...column,
|
|
1551
|
+
id: `${settings.id}_${rowIndex}_${column.id}`,
|
|
1552
|
+
parent: settings.id
|
|
1553
|
+
};
|
|
1554
|
+
|
|
1555
|
+
switch (column.type) {
|
|
1556
|
+
case 'checkbox':
|
|
1557
|
+
return (
|
|
1558
|
+
<div className={styles.fi__checkboxcontainer}>
|
|
1559
|
+
<input
|
|
1560
|
+
type="checkbox"
|
|
1561
|
+
checked={cellValue || false}
|
|
1562
|
+
data-id={settings.id}
|
|
1563
|
+
className={styles.fi__customcheckbox}
|
|
1564
|
+
onChange={(e) =>
|
|
1565
|
+
onChangeJsonTable && onChangeJsonTable(e, rowIndex, column.id, 'checkbox')
|
|
1566
|
+
}
|
|
1567
|
+
/>
|
|
1568
|
+
</div>
|
|
1569
|
+
);
|
|
1570
|
+
case 'html5_date':
|
|
1571
|
+
return (
|
|
1572
|
+
<input
|
|
1573
|
+
type="date"
|
|
1574
|
+
value={cellValue || ''}
|
|
1575
|
+
data-id={settings.id}
|
|
1576
|
+
className={styles.jsonTableInput}
|
|
1577
|
+
onChange={(e) =>
|
|
1578
|
+
onChangeJsonTable && onChangeJsonTable(e, rowIndex, column.id, 'date')
|
|
1579
|
+
}
|
|
1580
|
+
/>
|
|
1581
|
+
);
|
|
1582
|
+
case 'html5_time':
|
|
1583
|
+
return (
|
|
1584
|
+
<input
|
|
1585
|
+
type="time"
|
|
1586
|
+
value={cellValue || ''}
|
|
1587
|
+
data-id={settings.id}
|
|
1588
|
+
className={styles.jsonTableInput}
|
|
1589
|
+
onChange={(e) =>
|
|
1590
|
+
onChangeJsonTable && onChangeJsonTable(e, rowIndex, column.id, 'time')
|
|
1591
|
+
}
|
|
1592
|
+
/>
|
|
1593
|
+
);
|
|
1594
|
+
case 'html5_datetime':
|
|
1595
|
+
return (
|
|
1596
|
+
<input
|
|
1597
|
+
type="datetime-local"
|
|
1598
|
+
value={cellValue || ''}
|
|
1599
|
+
data-id={settings.id}
|
|
1600
|
+
className={styles.jsonTableInput}
|
|
1601
|
+
onChange={(e) =>
|
|
1602
|
+
onChangeJsonTable && onChangeJsonTable(e, rowIndex, column.id, 'datetime')
|
|
1603
|
+
}
|
|
1604
|
+
/>
|
|
1605
|
+
);
|
|
1606
|
+
case 'number':
|
|
1607
|
+
case 'decimal':
|
|
1608
|
+
// Special handling for interval numbers - make them auto-generated and read-only
|
|
1609
|
+
if (column.id === 'number' || column.autoGenerate) {
|
|
1610
|
+
return (
|
|
1611
|
+
<div className={styles.jsonTableDisplay}>
|
|
1612
|
+
{rowIndex + 1}
|
|
1613
|
+
</div>
|
|
1614
|
+
);
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1617
|
+
return (
|
|
1618
|
+
<input
|
|
1619
|
+
type="number"
|
|
1620
|
+
value={cellValue || ''}
|
|
1621
|
+
min={column.min}
|
|
1622
|
+
max={column.max}
|
|
1623
|
+
step={column.type === 'decimal' ? '0.01' : '1'}
|
|
1624
|
+
data-id={settings.id}
|
|
1625
|
+
className={styles.jsonTableInput}
|
|
1626
|
+
onChange={(e) =>
|
|
1627
|
+
onChangeJsonTable && onChangeJsonTable(e, rowIndex, column.id, 'number')
|
|
1628
|
+
}
|
|
1629
|
+
/>
|
|
1630
|
+
);
|
|
1631
|
+
case 'textarea':
|
|
1632
|
+
return (
|
|
1633
|
+
<textarea
|
|
1634
|
+
value={cellValue || ''}
|
|
1635
|
+
placeholder={column.placeholder || ''}
|
|
1636
|
+
data-id={settings.id}
|
|
1637
|
+
className={styles.jsonTableTextarea}
|
|
1638
|
+
onChange={(e) =>
|
|
1639
|
+
onChangeJsonTable && onChangeJsonTable(e, rowIndex, column.id, 'textarea')
|
|
1640
|
+
}
|
|
1641
|
+
rows={column.rows || 3}
|
|
1642
|
+
/>
|
|
1643
|
+
);
|
|
1644
|
+
case 'toggle':
|
|
1645
|
+
return (
|
|
1646
|
+
<div style={{ display: 'flex', justifyContent: 'center' }}>
|
|
1647
|
+
<Toggle
|
|
1648
|
+
checked={cellValue || false}
|
|
1649
|
+
onChange={(e) =>
|
|
1650
|
+
onChangeJsonTable && onChangeJsonTable(e, rowIndex, column.id, 'toggle')
|
|
1651
|
+
}
|
|
1652
|
+
/>
|
|
1653
|
+
</div>
|
|
1654
|
+
);
|
|
1655
|
+
case 'text':
|
|
1656
|
+
default:
|
|
1657
|
+
return (
|
|
1658
|
+
<input
|
|
1659
|
+
type="text"
|
|
1660
|
+
value={cellValue || ''}
|
|
1661
|
+
placeholder={column.placeholder || ''}
|
|
1662
|
+
data-id={settings.id}
|
|
1663
|
+
className={styles.jsonTableInput}
|
|
1664
|
+
onChange={(e) =>
|
|
1665
|
+
onChangeJsonTable && onChangeJsonTable(e, rowIndex, column.id, 'text')
|
|
1666
|
+
}
|
|
1667
|
+
/>
|
|
1668
|
+
);
|
|
1669
|
+
}
|
|
1670
|
+
};
|
|
1671
|
+
|
|
1672
|
+
return (
|
|
1673
|
+
<div>
|
|
1674
|
+
{settings.label && (
|
|
1675
|
+
<div className={styles.jsonTableLabel} style={{
|
|
1676
|
+
marginBottom: '0.75rem',
|
|
1677
|
+
fontWeight: '500',
|
|
1678
|
+
fontSize: '0.9rem',
|
|
1679
|
+
color: 'var(--paragraph-color)',
|
|
1680
|
+
display: 'flex',
|
|
1681
|
+
alignItems: 'center'
|
|
1682
|
+
}}>
|
|
1683
|
+
{settings.label}
|
|
1684
|
+
{settings.required && (
|
|
1685
|
+
<span style={{
|
|
1686
|
+
color: 'var(--danger-color, #dc3545)',
|
|
1687
|
+
marginLeft: '3px'
|
|
1688
|
+
}}>
|
|
1689
|
+
*
|
|
1690
|
+
</span>
|
|
1691
|
+
)}
|
|
1692
|
+
</div>
|
|
1693
|
+
)}
|
|
1694
|
+
<div className={styles.jsonTableContainer}>
|
|
1695
|
+
{jsonTableError && (
|
|
1696
|
+
<div className={styles.jsonTableError} style={{
|
|
1697
|
+
color: 'var(--danger-color, #dc3545)',
|
|
1698
|
+
marginBottom: '10px',
|
|
1699
|
+
padding: '8px 12px',
|
|
1700
|
+
backgroundColor: 'var(--danger-bg, #f8d7da)',
|
|
1701
|
+
border: '1px solid var(--danger-border, #f5c6cb)',
|
|
1702
|
+
borderRadius: 'var(--br, 4px)',
|
|
1703
|
+
fontSize: '0.875rem'
|
|
1704
|
+
}}>
|
|
1705
|
+
<strong>Invalid JSON:</strong> {jsonTableError}
|
|
1706
|
+
</div>
|
|
1707
|
+
)}
|
|
1708
|
+
|
|
1709
|
+
<div className={styles.jsonTableWrapper} style={{
|
|
1710
|
+
overflowX: 'auto',
|
|
1711
|
+
marginBottom: '1rem'
|
|
1712
|
+
}}>
|
|
1713
|
+
<table className={`${styles.contentTable} ${styles.jsonTable}`}>
|
|
1714
|
+
<thead>
|
|
1715
|
+
<tr>
|
|
1716
|
+
{settings.columns?.map((column, index) => (
|
|
1717
|
+
<th key={index} style={{
|
|
1718
|
+
width: column.width || 'auto',
|
|
1719
|
+
minWidth: column.minWidth || 'auto'
|
|
1720
|
+
}}>
|
|
1721
|
+
{column.label}
|
|
1722
|
+
{column.required && (
|
|
1723
|
+
<span style={{
|
|
1724
|
+
color: 'var(--danger-color, #dc3545)',
|
|
1725
|
+
marginLeft: '3px'
|
|
1726
|
+
}}>
|
|
1727
|
+
*
|
|
1728
|
+
</span>
|
|
1729
|
+
)}
|
|
1730
|
+
</th>
|
|
1731
|
+
))}
|
|
1732
|
+
{settings.deleteRowButton?.enabled !== false && (
|
|
1733
|
+
<th style={{ width: '60px', textAlign: 'center' }}>
|
|
1734
|
+
Actions
|
|
1735
|
+
</th>
|
|
1736
|
+
)}
|
|
1737
|
+
</tr>
|
|
1738
|
+
</thead>
|
|
1739
|
+
<tbody>
|
|
1740
|
+
{jsonTableData.map((row, rowIndex) => (
|
|
1741
|
+
<tr key={rowIndex}>
|
|
1742
|
+
{settings.columns?.map((column, columnIndex) => (
|
|
1743
|
+
<td key={columnIndex} style={{
|
|
1744
|
+
verticalAlign: 'top',
|
|
1745
|
+
padding: '8px 12px'
|
|
1746
|
+
}}>
|
|
1747
|
+
{renderJsonTableField(
|
|
1748
|
+
column,
|
|
1749
|
+
row[column.id],
|
|
1750
|
+
rowIndex,
|
|
1751
|
+
columnIndex
|
|
1752
|
+
)}
|
|
1753
|
+
</td>
|
|
1754
|
+
))}
|
|
1755
|
+
{settings.deleteRowButton?.enabled !== false && (
|
|
1756
|
+
<td style={{
|
|
1757
|
+
textAlign: 'center',
|
|
1758
|
+
verticalAlign: 'middle'
|
|
1759
|
+
}}>
|
|
1760
|
+
<button
|
|
1761
|
+
type="button"
|
|
1762
|
+
onClick={() =>
|
|
1763
|
+
onDeleteJsonTableRow && onDeleteJsonTableRow(rowIndex, settings.id)
|
|
1764
|
+
}
|
|
1765
|
+
className={styles.jsonTableDeleteBtn}
|
|
1766
|
+
title="Delete row"
|
|
1767
|
+
>
|
|
1768
|
+
<Trash2 size={14} />
|
|
1769
|
+
</button>
|
|
1770
|
+
</td>
|
|
1771
|
+
)}
|
|
1772
|
+
</tr>
|
|
1773
|
+
))}
|
|
1774
|
+
</tbody>
|
|
1775
|
+
</table>
|
|
1776
|
+
</div>
|
|
1777
|
+
|
|
1778
|
+
{settings.addRowButton?.enabled !== false && (
|
|
1779
|
+
<div className={styles.jsonTableActions}>
|
|
1780
|
+
<button
|
|
1781
|
+
type="button"
|
|
1782
|
+
onClick={() =>
|
|
1783
|
+
onAddJsonTableRow && onAddJsonTableRow(settings.id, settings.defaultRow || {})
|
|
1784
|
+
}
|
|
1785
|
+
className={styles.jsonTableAddBtn}
|
|
1786
|
+
>
|
|
1787
|
+
<CirclePlus size={16} />
|
|
1788
|
+
<span>{settings.addRowButton?.label || 'Add Row'}</span>
|
|
1789
|
+
</button>
|
|
1790
|
+
</div>
|
|
1791
|
+
)}
|
|
1792
|
+
|
|
1793
|
+
{hasValidationErrors && (
|
|
1794
|
+
<div className={styles.validationErrors}>
|
|
1795
|
+
{validationResult.errors.map((error, index) => (
|
|
1796
|
+
<div key={index} className={styles.validationError}>
|
|
1797
|
+
{error}
|
|
1798
|
+
</div>
|
|
1799
|
+
))}
|
|
1800
|
+
</div>
|
|
1801
|
+
)}
|
|
1802
|
+
</div>
|
|
1803
|
+
</div>
|
|
1804
|
+
);
|
|
1412
1805
|
case 'line-break':
|
|
1413
1806
|
return (
|
|
1414
1807
|
<div className={styles.formbreak}>
|
|
@@ -2246,6 +2639,7 @@ function Field({
|
|
|
2246
2639
|
'file',
|
|
2247
2640
|
'gallery',
|
|
2248
2641
|
'image',
|
|
2642
|
+
'json-table',
|
|
2249
2643
|
'line-break',
|
|
2250
2644
|
'multi-dropdown',
|
|
2251
2645
|
'multi-dropdown-ajax',
|
|
@@ -2690,7 +3084,7 @@ function Field({
|
|
|
2690
3084
|
<div className={formItemClass} style={formItemStyle} ref={containerRef}>
|
|
2691
3085
|
<label
|
|
2692
3086
|
className={
|
|
2693
|
-
['richeditor', 'textarea'].includes(settings.type)
|
|
3087
|
+
['richeditor', 'textarea', 'json-table'].includes(settings.type)
|
|
2694
3088
|
? ''
|
|
2695
3089
|
: styles.fi__label
|
|
2696
3090
|
}
|
package/src/components/Form.jsx
CHANGED
|
@@ -567,6 +567,188 @@ function Form({
|
|
|
567
567
|
}
|
|
568
568
|
};
|
|
569
569
|
|
|
570
|
+
// JSON Table handlers
|
|
571
|
+
const handleChangeJsonTable = (event, rowIndex, columnId, fieldType) => {
|
|
572
|
+
const fieldId = event.target.getAttribute('data-id');
|
|
573
|
+
let value = event.target.value;
|
|
574
|
+
|
|
575
|
+
// Handle different field types
|
|
576
|
+
if (fieldType === 'checkbox' || fieldType === 'toggle') {
|
|
577
|
+
value = event.target.checked;
|
|
578
|
+
} else if (fieldType === 'number') {
|
|
579
|
+
value = parseFloat(value) || 0;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
setFormData(prevData => {
|
|
583
|
+
const newData = { ...prevData };
|
|
584
|
+
|
|
585
|
+
// Get the current JSON value for this field
|
|
586
|
+
let currentValue = newData[fieldId];
|
|
587
|
+
if (typeof currentValue === 'string') {
|
|
588
|
+
try {
|
|
589
|
+
currentValue = JSON.parse(currentValue);
|
|
590
|
+
} catch (e) {
|
|
591
|
+
currentValue = [];
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
// Find field configuration from form settings
|
|
596
|
+
let fieldConfig = null;
|
|
597
|
+
if (formSettings && formSettings.fields) {
|
|
598
|
+
fieldConfig = formSettings.fields.find(f => f.id === fieldId);
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// Handle nested JSON path (e.g., intervals within galvanizing_intervals)
|
|
602
|
+
let tableData;
|
|
603
|
+
|
|
604
|
+
if (fieldConfig && fieldConfig.jsonPath && currentValue[fieldConfig.jsonPath]) {
|
|
605
|
+
tableData = [...currentValue[fieldConfig.jsonPath]];
|
|
606
|
+
} else if (Array.isArray(currentValue)) {
|
|
607
|
+
tableData = [...currentValue];
|
|
608
|
+
} else {
|
|
609
|
+
tableData = [];
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
// Update the specific cell
|
|
613
|
+
if (tableData[rowIndex]) {
|
|
614
|
+
tableData[rowIndex] = {
|
|
615
|
+
...tableData[rowIndex],
|
|
616
|
+
[columnId]: value
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
// Update the data structure
|
|
621
|
+
if (fieldConfig && fieldConfig.jsonPath) {
|
|
622
|
+
newData[fieldId] = {
|
|
623
|
+
...currentValue,
|
|
624
|
+
[fieldConfig.jsonPath]: tableData
|
|
625
|
+
};
|
|
626
|
+
} else {
|
|
627
|
+
newData[fieldId] = tableData;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
return newData;
|
|
631
|
+
});
|
|
632
|
+
};
|
|
633
|
+
|
|
634
|
+
const handleAddJsonTableRow = (fieldId, defaultRow = {}) => {
|
|
635
|
+
setFormData(prevData => {
|
|
636
|
+
const newData = { ...prevData };
|
|
637
|
+
let currentValue = newData[fieldId];
|
|
638
|
+
|
|
639
|
+
if (typeof currentValue === 'string') {
|
|
640
|
+
try {
|
|
641
|
+
currentValue = JSON.parse(currentValue);
|
|
642
|
+
} catch (e) {
|
|
643
|
+
currentValue = [];
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
// Find field configuration from form settings
|
|
648
|
+
let fieldConfig = null;
|
|
649
|
+
if (formSettings && formSettings.fields) {
|
|
650
|
+
fieldConfig = formSettings.fields.find(f => f.id === fieldId);
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
let tableData;
|
|
654
|
+
|
|
655
|
+
if (fieldConfig && fieldConfig.jsonPath && currentValue[fieldConfig.jsonPath]) {
|
|
656
|
+
tableData = [...currentValue[fieldConfig.jsonPath]];
|
|
657
|
+
} else if (Array.isArray(currentValue)) {
|
|
658
|
+
tableData = [...currentValue];
|
|
659
|
+
} else {
|
|
660
|
+
tableData = [];
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
// Add new row with auto-generated number
|
|
664
|
+
const newRow = {
|
|
665
|
+
...defaultRow,
|
|
666
|
+
number: tableData.length + 1
|
|
667
|
+
};
|
|
668
|
+
tableData.push(newRow);
|
|
669
|
+
|
|
670
|
+
// Update the data structure
|
|
671
|
+
if (fieldConfig && fieldConfig.jsonPath) {
|
|
672
|
+
newData[fieldId] = {
|
|
673
|
+
...currentValue,
|
|
674
|
+
[fieldConfig.jsonPath]: tableData
|
|
675
|
+
};
|
|
676
|
+
} else {
|
|
677
|
+
newData[fieldId] = tableData;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
return newData;
|
|
681
|
+
});
|
|
682
|
+
};
|
|
683
|
+
|
|
684
|
+
const handleDeleteJsonTableRow = (rowIndex, fieldId) => {
|
|
685
|
+
confirmDialog({
|
|
686
|
+
title: 'Delete Interval',
|
|
687
|
+
message: 'Are you sure you want to delete this interval?',
|
|
688
|
+
buttons: [
|
|
689
|
+
{
|
|
690
|
+
label: 'Yes',
|
|
691
|
+
onClick: () => {
|
|
692
|
+
setFormData(prevData => {
|
|
693
|
+
const newData = { ...prevData };
|
|
694
|
+
let currentValue = newData[fieldId];
|
|
695
|
+
|
|
696
|
+
if (typeof currentValue === 'string') {
|
|
697
|
+
try {
|
|
698
|
+
currentValue = JSON.parse(currentValue);
|
|
699
|
+
} catch (e) {
|
|
700
|
+
currentValue = [];
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
// Find field configuration from form settings
|
|
705
|
+
let fieldConfig = null;
|
|
706
|
+
if (formSettings && formSettings.fields) {
|
|
707
|
+
fieldConfig = formSettings.fields.find(f => f.id === fieldId);
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
let tableData;
|
|
711
|
+
|
|
712
|
+
if (fieldConfig && fieldConfig.jsonPath && currentValue[fieldConfig.jsonPath]) {
|
|
713
|
+
tableData = [...currentValue[fieldConfig.jsonPath]];
|
|
714
|
+
} else if (Array.isArray(currentValue)) {
|
|
715
|
+
tableData = [...currentValue];
|
|
716
|
+
} else {
|
|
717
|
+
tableData = [];
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
// Remove row
|
|
721
|
+
tableData.splice(rowIndex, 1);
|
|
722
|
+
|
|
723
|
+
// Re-number the remaining rows
|
|
724
|
+
tableData.forEach((row, index) => {
|
|
725
|
+
row.number = index + 1;
|
|
726
|
+
});
|
|
727
|
+
|
|
728
|
+
// Update the data structure
|
|
729
|
+
if (fieldConfig && fieldConfig.jsonPath) {
|
|
730
|
+
newData[fieldId] = {
|
|
731
|
+
...currentValue,
|
|
732
|
+
[fieldConfig.jsonPath]: tableData
|
|
733
|
+
};
|
|
734
|
+
} else {
|
|
735
|
+
newData[fieldId] = tableData;
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
return newData;
|
|
739
|
+
});
|
|
740
|
+
}
|
|
741
|
+
},
|
|
742
|
+
{
|
|
743
|
+
label: 'No',
|
|
744
|
+
onClick: () => {
|
|
745
|
+
// User cancelled - do nothing
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
]
|
|
749
|
+
});
|
|
750
|
+
};
|
|
751
|
+
|
|
570
752
|
const autocompleteSelect = (place, id) => {
|
|
571
753
|
const { address, text, context } = place;
|
|
572
754
|
|
|
@@ -995,6 +1177,132 @@ function Form({
|
|
|
995
1177
|
// Update the form data state with the auto values
|
|
996
1178
|
setFormData(updatedFormData);
|
|
997
1179
|
|
|
1180
|
+
// Custom validation for json-table fields
|
|
1181
|
+
fields.forEach((item) => {
|
|
1182
|
+
if (item.type === 'json-table' && item.validation?.customValidation) {
|
|
1183
|
+
try {
|
|
1184
|
+
let jsonTableData = [];
|
|
1185
|
+
const fieldValue = formData[item.id];
|
|
1186
|
+
|
|
1187
|
+
if (fieldValue && fieldValue !== 'null' && fieldValue !== '') {
|
|
1188
|
+
const parsedValue = typeof fieldValue === 'string' ? JSON.parse(fieldValue) : fieldValue;
|
|
1189
|
+
|
|
1190
|
+
// Handle jsonPath setting to extract nested data
|
|
1191
|
+
if (item.jsonPath && parsedValue[item.jsonPath]) {
|
|
1192
|
+
jsonTableData = Array.isArray(parsedValue[item.jsonPath])
|
|
1193
|
+
? parsedValue[item.jsonPath]
|
|
1194
|
+
: [];
|
|
1195
|
+
} else if (Array.isArray(parsedValue)) {
|
|
1196
|
+
jsonTableData = parsedValue;
|
|
1197
|
+
} else if (typeof parsedValue === 'object' && parsedValue !== null) {
|
|
1198
|
+
jsonTableData = [parsedValue];
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
// Perform validation using the same logic as in Field.jsx
|
|
1203
|
+
const validateJsonTable = (data, validationConfig) => {
|
|
1204
|
+
if (!validationConfig || !validationConfig.customValidation) {
|
|
1205
|
+
return { isValid: true, errors: [] };
|
|
1206
|
+
}
|
|
1207
|
+
const { type, startTimeField, endTimeField, message } = validationConfig.customValidation;
|
|
1208
|
+
const errors = [];
|
|
1209
|
+
|
|
1210
|
+
if (type === '24hour_coverage') {
|
|
1211
|
+
// Convert time strings to minutes for easier calculation
|
|
1212
|
+
const timeToMinutes = (timeStr) => {
|
|
1213
|
+
if (!timeStr) return null;
|
|
1214
|
+
const [hours, minutes] = timeStr.split(':').map(Number);
|
|
1215
|
+
return hours * 60 + minutes;
|
|
1216
|
+
};
|
|
1217
|
+
|
|
1218
|
+
// Sort intervals by start time
|
|
1219
|
+
const sortedIntervals = [...data].sort((a, b) => {
|
|
1220
|
+
const aStart = timeToMinutes(a[startTimeField]);
|
|
1221
|
+
const bStart = timeToMinutes(b[startTimeField]);
|
|
1222
|
+
return aStart - bStart;
|
|
1223
|
+
});
|
|
1224
|
+
|
|
1225
|
+
let totalCoverage = 0;
|
|
1226
|
+
let previousEnd = null;
|
|
1227
|
+
|
|
1228
|
+
for (let i = 0; i < sortedIntervals.length; i++) {
|
|
1229
|
+
const interval = sortedIntervals[i];
|
|
1230
|
+
const startMinutes = timeToMinutes(interval[startTimeField]);
|
|
1231
|
+
const endMinutes = timeToMinutes(interval[endTimeField]);
|
|
1232
|
+
|
|
1233
|
+
if (startMinutes === null || endMinutes === null) {
|
|
1234
|
+
errors.push(`Interval ${i + 1}: Missing start or end time`);
|
|
1235
|
+
continue;
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
// Handle cross-midnight intervals (e.g., 18:00 to 06:00)
|
|
1239
|
+
let duration;
|
|
1240
|
+
if (endMinutes <= startMinutes) {
|
|
1241
|
+
duration = (endMinutes + 1440) - startMinutes;
|
|
1242
|
+
} else {
|
|
1243
|
+
duration = endMinutes - startMinutes;
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
totalCoverage += duration;
|
|
1247
|
+
|
|
1248
|
+
// Check for gaps (except for the first interval)
|
|
1249
|
+
if (previousEnd !== null && startMinutes !== previousEnd) {
|
|
1250
|
+
const gapSize = startMinutes > previousEnd
|
|
1251
|
+
? startMinutes - previousEnd
|
|
1252
|
+
: (startMinutes + 1440) - previousEnd;
|
|
1253
|
+
|
|
1254
|
+
if (gapSize > 0 && gapSize < 1440) {
|
|
1255
|
+
errors.push(`Gap detected between intervals: ${Math.floor(gapSize / 60)}h ${gapSize % 60}m`);
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
// Update previousEnd, handling cross-midnight
|
|
1260
|
+
previousEnd = endMinutes <= startMinutes ? endMinutes + 1440 : endMinutes;
|
|
1261
|
+
if (previousEnd >= 1440) previousEnd -= 1440;
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
// Check if total coverage equals 24 hours (1440 minutes)
|
|
1265
|
+
if (Math.abs(totalCoverage - 1440) > 1) {
|
|
1266
|
+
errors.push(`Total coverage is ${Math.floor(totalCoverage / 60)}h ${totalCoverage % 60}m. Required: 24h 0m`);
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
// Check for overlaps
|
|
1270
|
+
for (let i = 0; i < sortedIntervals.length - 1; i++) {
|
|
1271
|
+
const current = sortedIntervals[i];
|
|
1272
|
+
const next = sortedIntervals[i + 1];
|
|
1273
|
+
|
|
1274
|
+
const currentStart = timeToMinutes(current[startTimeField]);
|
|
1275
|
+
const currentEnd = timeToMinutes(current[endTimeField]);
|
|
1276
|
+
const nextStart = timeToMinutes(next[startTimeField]);
|
|
1277
|
+
|
|
1278
|
+
const currentActualEnd = currentEnd <= currentStart ? currentEnd + 1440 : currentEnd;
|
|
1279
|
+
const nextActualStart = nextStart;
|
|
1280
|
+
|
|
1281
|
+
if (currentActualEnd > nextActualStart && currentActualEnd < nextActualStart + 1440) {
|
|
1282
|
+
const overlapMinutes = currentActualEnd - nextActualStart;
|
|
1283
|
+
errors.push(`Overlap detected: ${Math.floor(overlapMinutes / 60)}h ${overlapMinutes % 60}m between intervals ${i + 1} and ${i + 2}`);
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
return {
|
|
1289
|
+
isValid: errors.length === 0,
|
|
1290
|
+
errors: errors.length > 0 ? [message, ...errors] : []
|
|
1291
|
+
};
|
|
1292
|
+
};
|
|
1293
|
+
|
|
1294
|
+
const validationResult = validateJsonTable(jsonTableData, item.validation);
|
|
1295
|
+
if (!validationResult.isValid) {
|
|
1296
|
+
validation += `${item.label}: ${validationResult.errors.join(', ')}<br/>`;
|
|
1297
|
+
_inputClass[item.id] = styles.inputError;
|
|
1298
|
+
}
|
|
1299
|
+
} catch (e) {
|
|
1300
|
+
validation += `${item.label}: Invalid configuration data<br/>`;
|
|
1301
|
+
_inputClass[item.id] = styles.inputError;
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
});
|
|
1305
|
+
|
|
998
1306
|
if (validation) {
|
|
999
1307
|
toast.error(<div>{parse(validation)}</div>);
|
|
1000
1308
|
} else {
|
|
@@ -1856,6 +2164,9 @@ function Form({
|
|
|
1856
2164
|
handleChangeSignature
|
|
1857
2165
|
}
|
|
1858
2166
|
onChangeToggle={handleChangeToggle}
|
|
2167
|
+
onChangeJsonTable={handleChangeJsonTable}
|
|
2168
|
+
onAddJsonTableRow={handleAddJsonTableRow}
|
|
2169
|
+
onDeleteJsonTableRow={handleDeleteJsonTableRow}
|
|
1859
2170
|
onFileDownload={handleFileDownload}
|
|
1860
2171
|
settings={item}
|
|
1861
2172
|
setFormData={setFormData}
|
|
@@ -2498,6 +2809,9 @@ function Form({
|
|
|
2498
2809
|
}
|
|
2499
2810
|
onChangeSelect={handleChangeSelect}
|
|
2500
2811
|
onChangeToggle={handleChangeToggle}
|
|
2812
|
+
onChangeJsonTable={handleChangeJsonTable}
|
|
2813
|
+
onAddJsonTableRow={handleAddJsonTableRow}
|
|
2814
|
+
onDeleteJsonTableRow={handleDeleteJsonTableRow}
|
|
2501
2815
|
onFileDownload={handleFileDownload}
|
|
2502
2816
|
settings={item}
|
|
2503
2817
|
setFormData={setFormData}
|
|
@@ -1385,6 +1385,223 @@ input[type='file']:hover {
|
|
|
1385
1385
|
box-shadow: none; /* Match react-select no-shadow pattern */
|
|
1386
1386
|
}
|
|
1387
1387
|
|
|
1388
|
+
/* JSON Table Field Styles */
|
|
1389
|
+
.jsonTableLabel {
|
|
1390
|
+
margin-bottom: 0.75rem;
|
|
1391
|
+
font-weight: 500;
|
|
1392
|
+
font-size: 0.9rem;
|
|
1393
|
+
color: var(--paragraph-color);
|
|
1394
|
+
display: flex;
|
|
1395
|
+
align-items: center;
|
|
1396
|
+
line-height: 1.3;
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
.jsonTableContainer {
|
|
1400
|
+
width: 100%;
|
|
1401
|
+
border: 1px solid var(--border-color, #e9ecef);
|
|
1402
|
+
border-radius: var(--br, 6px);
|
|
1403
|
+
background: var(--item-color, #fff);
|
|
1404
|
+
overflow: hidden;
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
.jsonTable {
|
|
1408
|
+
width: 100%;
|
|
1409
|
+
border-collapse: collapse;
|
|
1410
|
+
font-size: 0.85rem;
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
.jsonTable th {
|
|
1414
|
+
background: var(--primary-color, #007bff);
|
|
1415
|
+
color: var(--secondary-color, #fff);
|
|
1416
|
+
font-weight: 600;
|
|
1417
|
+
padding: 0.75rem 0.5rem;
|
|
1418
|
+
text-align: left;
|
|
1419
|
+
border-bottom: 2px solid rgba(var(--primary-color-rgb, 0, 123, 255), 0.8);
|
|
1420
|
+
font-size: 0.8rem;
|
|
1421
|
+
line-height: 1.2;
|
|
1422
|
+
letter-spacing: 0.02em;
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
.jsonTable td {
|
|
1426
|
+
padding: 0.5rem;
|
|
1427
|
+
border-bottom: 1px solid var(--table-border, #dee2e6);
|
|
1428
|
+
vertical-align: middle;
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
.jsonTable tbody tr:hover {
|
|
1432
|
+
background: rgba(var(--primary-color-rgb, 0, 123, 255), 0.03);
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
.jsonTableInput {
|
|
1436
|
+
width: 100%;
|
|
1437
|
+
padding: 0.4rem 0.6rem;
|
|
1438
|
+
border: 1px solid rgba(var(--border-color-rgb, 233, 236, 239), 0.6);
|
|
1439
|
+
border-radius: calc(var(--br, 4px) - 1px);
|
|
1440
|
+
background: var(--tertiary-color, #fff);
|
|
1441
|
+
color: var(--paragraph-color);
|
|
1442
|
+
font-size: 0.8rem;
|
|
1443
|
+
line-height: 1.3;
|
|
1444
|
+
transition: all 0.15s ease;
|
|
1445
|
+
box-sizing: border-box;
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
.jsonTableInput:focus {
|
|
1449
|
+
outline: none;
|
|
1450
|
+
border-color: var(--primary-color, #007bff);
|
|
1451
|
+
background: var(--item-color, #fff);
|
|
1452
|
+
box-shadow: 0 0 0 2px rgba(var(--primary-color-rgb, 0, 123, 255), 0.1);
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
.jsonTableInput:hover {
|
|
1456
|
+
border-color: rgba(var(--paragraph-color-rgb, 73, 80, 87), 0.5);
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
.jsonTableTextarea {
|
|
1460
|
+
width: 100%;
|
|
1461
|
+
padding: 0.4rem 0.6rem;
|
|
1462
|
+
border: 1px solid rgba(var(--border-color-rgb, 233, 236, 239), 0.6);
|
|
1463
|
+
border-radius: calc(var(--br, 4px) - 1px);
|
|
1464
|
+
background: var(--tertiary-color, #fff);
|
|
1465
|
+
color: var(--paragraph-color);
|
|
1466
|
+
font-size: 0.8rem;
|
|
1467
|
+
line-height: 1.4;
|
|
1468
|
+
transition: all 0.15s ease;
|
|
1469
|
+
resize: vertical;
|
|
1470
|
+
min-height: 2.2rem;
|
|
1471
|
+
font-family: inherit;
|
|
1472
|
+
box-sizing: border-box;
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
.jsonTableTextarea:focus {
|
|
1476
|
+
outline: none;
|
|
1477
|
+
border-color: var(--primary-color, #007bff);
|
|
1478
|
+
background: var(--item-color, #fff);
|
|
1479
|
+
box-shadow: 0 0 0 2px rgba(var(--primary-color-rgb, 0, 123, 255), 0.1);
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
.jsonTableTextarea:hover {
|
|
1483
|
+
border-color: rgba(var(--paragraph-color-rgb, 73, 80, 87), 0.5);
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1486
|
+
.jsonTableDisplay {
|
|
1487
|
+
padding: 0.85rem 0.3rem;
|
|
1488
|
+
color: var(--paragraph-color);
|
|
1489
|
+
font-size: 0.8rem;
|
|
1490
|
+
font-weight: 500;
|
|
1491
|
+
text-align: center;
|
|
1492
|
+
background: rgba(var(--primary-color-rgb, 0, 123, 255), 0.05);
|
|
1493
|
+
border-radius: calc(var(--br, 4px) - 1px);
|
|
1494
|
+
border: 1px solid rgba(var(--primary-color-rgb, 0, 123, 255), 0.15);
|
|
1495
|
+
line-height: 1.3;
|
|
1496
|
+
user-select: none;
|
|
1497
|
+
height: auto;
|
|
1498
|
+
display: flex;
|
|
1499
|
+
align-items: center;
|
|
1500
|
+
justify-content: center;
|
|
1501
|
+
box-sizing: border-box;
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
.validationErrors {
|
|
1505
|
+
margin-top: 0.75rem;
|
|
1506
|
+
padding: 0.75rem;
|
|
1507
|
+
border-radius: var(--br, 4px);
|
|
1508
|
+
border: 1px solid var(--danger-color, #dc3545);
|
|
1509
|
+
background: rgba(var(--danger-color-rgb, 220, 53, 69), 0.05);
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
.validationError {
|
|
1513
|
+
color: var(--danger-color, #dc3545);
|
|
1514
|
+
font-size: 0.875rem;
|
|
1515
|
+
margin: 0 0 0.5rem 0;
|
|
1516
|
+
font-weight: 500;
|
|
1517
|
+
|
|
1518
|
+
&:last-child {
|
|
1519
|
+
margin-bottom: 0;
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1522
|
+
&:first-child {
|
|
1523
|
+
font-weight: 600;
|
|
1524
|
+
margin-bottom: 0.75rem;
|
|
1525
|
+
padding-bottom: 0.5rem;
|
|
1526
|
+
border-bottom: 1px solid rgba(var(--danger-color-rgb, 220, 53, 69), 0.2);
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
.jsonTableActions {
|
|
1531
|
+
display: flex;
|
|
1532
|
+
justify-content: center;
|
|
1533
|
+
align-items: center;
|
|
1534
|
+
gap: 0.5rem;
|
|
1535
|
+
min-width: 4rem;
|
|
1536
|
+
padding: 0.25rem;
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1539
|
+
.jsonTableDeleteBtn {
|
|
1540
|
+
display: flex;
|
|
1541
|
+
align-items: center;
|
|
1542
|
+
justify-content: center;
|
|
1543
|
+
width: 1.8rem;
|
|
1544
|
+
height: 1.8rem;
|
|
1545
|
+
padding: 0;
|
|
1546
|
+
background: rgba(var(--danger-color-rgb, 220, 53, 69), 0.08);
|
|
1547
|
+
border: 1px solid rgba(var(--danger-color-rgb, 220, 53, 69), 0.2);
|
|
1548
|
+
border-radius: calc(var(--br, 4px) - 1px);
|
|
1549
|
+
color: var(--danger-color, #dc3545);
|
|
1550
|
+
cursor: pointer;
|
|
1551
|
+
transition: all 0.15s ease;
|
|
1552
|
+
font-size: 0.75rem;
|
|
1553
|
+
line-height: 1;
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
.jsonTableDeleteBtn:hover {
|
|
1557
|
+
background: var(--danger-color, #dc3545);
|
|
1558
|
+
color: var(--secondary-color, #fff);
|
|
1559
|
+
border-color: var(--danger-color, #dc3545);
|
|
1560
|
+
transform: translateY(-1px);
|
|
1561
|
+
box-shadow: 0 2px 4px rgba(var(--danger-color-rgb, 220, 53, 69), 0.3);
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1564
|
+
.jsonTableDeleteBtn:focus {
|
|
1565
|
+
outline: none;
|
|
1566
|
+
box-shadow: 0 0 0 2px rgba(var(--danger-color-rgb, 220, 53, 69), 0.25);
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
.jsonTableAddBtn {
|
|
1570
|
+
display: inline-flex;
|
|
1571
|
+
align-items: center;
|
|
1572
|
+
gap: 0.4rem;
|
|
1573
|
+
padding: 0.5rem 0.85rem;
|
|
1574
|
+
margin-top: 0.75rem;
|
|
1575
|
+
background: rgba(var(--primary-color-rgb, 0, 123, 255), 0.08);
|
|
1576
|
+
border: 1px solid rgba(var(--primary-color-rgb, 0, 123, 255), 0.25);
|
|
1577
|
+
border-radius: var(--br, 4px);
|
|
1578
|
+
color: var(--primary-color, #007bff);
|
|
1579
|
+
cursor: pointer;
|
|
1580
|
+
transition: all 0.15s ease;
|
|
1581
|
+
font-size: 0.8rem;
|
|
1582
|
+
font-weight: 500;
|
|
1583
|
+
text-decoration: none;
|
|
1584
|
+
outline: none;
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1587
|
+
.jsonTableAddBtn:hover {
|
|
1588
|
+
background: var(--primary-color, #007bff);
|
|
1589
|
+
color: var(--secondary-color, #fff);
|
|
1590
|
+
border-color: var(--primary-color, #007bff);
|
|
1591
|
+
transform: translateY(-1px);
|
|
1592
|
+
box-shadow: 0 2px 6px rgba(var(--primary-color-rgb, 0, 123, 255), 0.25);
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1595
|
+
.jsonTableAddBtn:focus {
|
|
1596
|
+
outline: none;
|
|
1597
|
+
box-shadow: 0 0 0 2px rgba(var(--primary-color-rgb, 0, 123, 255), 0.25);
|
|
1598
|
+
}
|
|
1599
|
+
|
|
1600
|
+
.jsonTableAddBtn svg {
|
|
1601
|
+
width: 0.9rem;
|
|
1602
|
+
height: 0.9rem;
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1388
1605
|
/* Dark mode support */
|
|
1389
1606
|
@media (prefers-color-scheme: dark) {
|
|
1390
1607
|
.emailSuggestions {
|
|
@@ -1401,4 +1618,45 @@ input[type='file']:hover {
|
|
|
1401
1618
|
background: var(--dark-primary-color, #3182ce);
|
|
1402
1619
|
color: var(--dark-primary-text-color, #fff);
|
|
1403
1620
|
}
|
|
1621
|
+
|
|
1622
|
+
.jsonTableLabel {
|
|
1623
|
+
color: var(--dark-text-color, #f9fafb);
|
|
1624
|
+
}
|
|
1625
|
+
|
|
1626
|
+
.jsonTableContainer {
|
|
1627
|
+
background: var(--dark-item-color, #2d3748);
|
|
1628
|
+
border-color: var(--dark-border-color, #4a5568);
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
.jsonTable th {
|
|
1632
|
+
background: var(--dark-primary-color, #1e40af);
|
|
1633
|
+
color: var(--dark-secondary-color, #f9fafb);
|
|
1634
|
+
border-bottom-color: rgba(var(--dark-primary-color-rgb, 30, 64, 175), 0.8);
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1637
|
+
.jsonTable td {
|
|
1638
|
+
border-bottom-color: var(--dark-table-border, #6b7280);
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1641
|
+
.jsonTableInput,
|
|
1642
|
+
.jsonTableTextarea {
|
|
1643
|
+
background: var(--dark-tertiary-color, #374151);
|
|
1644
|
+
border-color: var(--dark-border-color, #6b7280);
|
|
1645
|
+
color: var(--dark-text-color, #f9fafb);
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1648
|
+
.jsonTableDisplay {
|
|
1649
|
+
background: rgba(var(--dark-primary-color-rgb, 30, 64, 175), 0.15);
|
|
1650
|
+
border-color: rgba(var(--dark-primary-color-rgb, 30, 64, 175), 0.3);
|
|
1651
|
+
color: var(--dark-text-color, #f9fafb);
|
|
1652
|
+
}
|
|
1653
|
+
|
|
1654
|
+
.validationErrors {
|
|
1655
|
+
background: rgba(var(--danger-color-rgb, 220, 53, 69), 0.1);
|
|
1656
|
+
border-color: var(--danger-color, #dc3545);
|
|
1657
|
+
}
|
|
1658
|
+
|
|
1659
|
+
.validationError {
|
|
1660
|
+
color: var(--danger-color, #dc3545);
|
|
1661
|
+
}
|
|
1404
1662
|
}
|