@visns-studio/visns-components 5.6.2 → 5.6.4

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 CHANGED
@@ -84,7 +84,7 @@
84
84
  "react-dom": "^17.0.0 || ^18.0.0"
85
85
  },
86
86
  "name": "@visns-studio/visns-components",
87
- "version": "5.6.2",
87
+ "version": "5.6.4",
88
88
  "description": "Various packages to assist in the development of our Custom Applications.",
89
89
  "main": "src/index.js",
90
90
  "files": [
@@ -35,28 +35,64 @@ const CustomDownload = (
35
35
  }
36
36
  }
37
37
 
38
- options = {
39
- method: method,
40
- data: formData,
41
- headers: headers,
42
- url: url,
43
- responseType: 'blob',
44
- };
45
-
38
+ // First make a request to check if the request is valid
46
39
  return trackPromise(
47
40
  new Promise((resolve, reject) => {
48
- axios(options)
41
+ // First, make a request without responseType: 'blob' to check for errors
42
+ axios({
43
+ method: method,
44
+ data: formData,
45
+ headers: headers,
46
+ url: url,
47
+ })
49
48
  .then((response) => {
50
- if (successCallback) {
51
- successCallback(response.data);
52
- } else {
53
- const error = response.data.error;
49
+ // If the response is JSON and contains an error, handle it
50
+ if (response.data && response.data.success === false) {
51
+ let errorMessage =
52
+ response.data.message || 'Unknown error';
53
+ if (response.data.error) {
54
+ errorMessage += ': ' + response.data.error;
55
+ }
54
56
 
55
- if (error && error !== '') {
56
- toast.error(<div>{parse(error)}</div>);
57
+ if (errorCallback) {
58
+ errorCallback(errorMessage);
59
+ } else {
60
+ toast.error(<div>{parse(errorMessage)}</div>);
57
61
  }
62
+
63
+ // Resolve with the error response
64
+ resolve(response);
65
+ } else {
66
+ // If no error, make the actual blob request
67
+ axios({
68
+ method: method,
69
+ data: formData,
70
+ headers: headers,
71
+ url: url,
72
+ responseType: 'blob',
73
+ })
74
+ .then((blobResponse) => {
75
+ if (successCallback) {
76
+ successCallback(blobResponse.data);
77
+ }
78
+ resolve(blobResponse);
79
+ })
80
+ .catch((blobError) => {
81
+ let errorMessage =
82
+ blobError.message ||
83
+ 'Error downloading file';
84
+
85
+ if (errorCallback) {
86
+ errorCallback(errorMessage);
87
+ } else {
88
+ toast.error(
89
+ <div>{parse(errorMessage)}</div>
90
+ );
91
+ }
92
+
93
+ reject(blobError);
94
+ });
58
95
  }
59
- resolve(response);
60
96
  })
61
97
  .catch((error) => {
62
98
  let errorMessage = '';
@@ -177,6 +177,96 @@ const isJsonValue = (value) => {
177
177
  return typeof value === 'object' && value !== null;
178
178
  };
179
179
 
180
+ // Helper function to validate filter criteria
181
+ const validateFilterCriteria = (filterCriteria) => {
182
+ // If it's already a flat array (legacy format)
183
+ if (Array.isArray(filterCriteria)) {
184
+ // Check each filter in the array
185
+ for (let i = 0; i < filterCriteria.length; i++) {
186
+ const filter = filterCriteria[i];
187
+
188
+ // Skip validation for IS NULL and IS NOT NULL operators which don't need a value
189
+ if (
190
+ filter.operator === 'IS NULL' ||
191
+ filter.operator === 'IS NOT NULL'
192
+ ) {
193
+ continue;
194
+ }
195
+
196
+ // Check if any required field is missing
197
+ if (
198
+ !filter.table ||
199
+ !filter.column ||
200
+ !filter.operator ||
201
+ filter.value === undefined ||
202
+ filter.value === null ||
203
+ filter.value === ''
204
+ ) {
205
+ return {
206
+ valid: false,
207
+ message: `Filter #${
208
+ i + 1
209
+ } is missing required fields (table, column, operator, or value)`,
210
+ };
211
+ }
212
+ }
213
+ return { valid: true };
214
+ }
215
+
216
+ // If it's the new structure with groups and operators
217
+ if (filterCriteria.groups && Array.isArray(filterCriteria.groups)) {
218
+ // Process each group
219
+ for (
220
+ let groupIndex = 0;
221
+ groupIndex < filterCriteria.groups.length;
222
+ groupIndex++
223
+ ) {
224
+ const group = filterCriteria.groups[groupIndex];
225
+
226
+ // Only validate non-empty groups
227
+ if (group.filters && group.filters.length > 0) {
228
+ // Check all filters in this group
229
+ for (
230
+ let filterIndex = 0;
231
+ filterIndex < group.filters.length;
232
+ filterIndex++
233
+ ) {
234
+ const filter = group.filters[filterIndex];
235
+
236
+ // Skip validation for IS NULL and IS NOT NULL operators which don't need a value
237
+ if (
238
+ filter.operator === 'IS NULL' ||
239
+ filter.operator === 'IS NOT NULL'
240
+ ) {
241
+ continue;
242
+ }
243
+
244
+ // Check if any required field is missing
245
+ if (
246
+ !filter.table ||
247
+ !filter.column ||
248
+ !filter.operator ||
249
+ filter.value === undefined ||
250
+ filter.value === null ||
251
+ filter.value === ''
252
+ ) {
253
+ return {
254
+ valid: false,
255
+ message: `Filter #${filterIndex + 1} in group #${
256
+ groupIndex + 1
257
+ } is missing required fields (table, column, operator, or value)`,
258
+ };
259
+ }
260
+ }
261
+ }
262
+ }
263
+ return { valid: true };
264
+ }
265
+
266
+ // If it's neither a flat array nor the new structure, consider it valid (empty)
267
+ return { valid: true };
268
+ };
269
+
180
270
  // Helper function to convert the enhanced filter structure to a flat array for the backend
181
271
  const flattenFilterCriteria = (filterCriteria) => {
182
272
  // If it's already a flat array (legacy format), return it as is
@@ -474,11 +564,17 @@ const GenericReport = ({ setting = {} }) => {
474
564
  const [gridColumns, setGridColumns] = useState([]);
475
565
  const [isLoading, setIsLoading] = useState(false);
476
566
 
567
+ // State for JSON field keys
568
+ const [jsonFieldKeys, setJsonFieldKeys] = useState({});
569
+ const [loadingJsonKeys, setLoadingJsonKeys] = useState({});
570
+ const [showJsonKeySelector, setShowJsonKeySelector] = useState({});
571
+
477
572
  // Extract additional settings with defaults
478
573
  const {
479
574
  reportsUrl = '/ajax/reportBuilder/reports',
480
575
  executeUrl = '/ajax/reportBuilder/execute',
481
576
  suggestedJoinsUrl = '/ajax/reportBuilder/getSuggestedJoins',
577
+ jsonFieldKeysUrl = '/ajax/reportBuilder/getJsonFieldKeys',
482
578
  } = setting;
483
579
 
484
580
  // Fetch all database tables and saved reports on component mount
@@ -744,9 +840,40 @@ const GenericReport = ({ setting = {} }) => {
744
840
  setSelectedColumns(updatedColumns);
745
841
  };
746
842
 
843
+ // Start editing a column name
844
+ const handleStartEditColumn = (index) => {
845
+ const column = selectedColumns[index];
846
+ setEditingColumnIndex(index);
847
+ setEditingColumnName(column.alias || column.displayName);
848
+ };
849
+
850
+ // Save the edited column name
851
+ const handleSaveColumnName = () => {
852
+ if (editingColumnIndex !== null) {
853
+ const updatedColumns = [...selectedColumns];
854
+ updatedColumns[editingColumnIndex] = {
855
+ ...updatedColumns[editingColumnIndex],
856
+ alias: editingColumnName,
857
+ };
858
+ setSelectedColumns(updatedColumns);
859
+ setEditingColumnIndex(null);
860
+ setEditingColumnName('');
861
+ }
862
+ };
863
+
864
+ // Cancel column name editing
865
+ const handleCancelEditColumn = () => {
866
+ setEditingColumnIndex(null);
867
+ setEditingColumnName('');
868
+ };
869
+
747
870
  // State for drag and drop
748
871
  const [draggedItemIndex, setDraggedItemIndex] = useState(null);
749
872
 
873
+ // State for column renaming
874
+ const [editingColumnIndex, setEditingColumnIndex] = useState(null);
875
+ const [editingColumnName, setEditingColumnName] = useState('');
876
+
750
877
  // Handle drag start for column reordering
751
878
  const handleDragStart = (e, index) => {
752
879
  setDraggedItemIndex(index);
@@ -1051,6 +1178,118 @@ const GenericReport = ({ setting = {} }) => {
1051
1178
  setShowFilteringSection(true);
1052
1179
  };
1053
1180
 
1181
+ // Fetch JSON field keys for a specific table and column
1182
+ const fetchJsonFieldKeys = async (tableName, columnName) => {
1183
+ const cacheKey = `${tableName}.${columnName}`;
1184
+
1185
+ // Check if we're already loading this data
1186
+ if (loadingJsonKeys[cacheKey]) {
1187
+ return;
1188
+ }
1189
+
1190
+ // Check if we already have this data cached
1191
+ if (jsonFieldKeys[cacheKey]) {
1192
+ return;
1193
+ }
1194
+
1195
+ // Set loading state
1196
+ setLoadingJsonKeys((prev) => ({
1197
+ ...prev,
1198
+ [cacheKey]: true,
1199
+ }));
1200
+
1201
+ try {
1202
+ const res = await CustomFetch(jsonFieldKeysUrl, 'POST', {
1203
+ table: tableName,
1204
+ column: columnName,
1205
+ limit: 100, // Analyze up to 100 records
1206
+ });
1207
+
1208
+ if (res.data?.success && res.data?.data?.keys) {
1209
+ // Store the keys in state
1210
+ setJsonFieldKeys((prev) => ({
1211
+ ...prev,
1212
+ [cacheKey]: res.data.data.keys,
1213
+ }));
1214
+ } else {
1215
+ console.warn(
1216
+ `No JSON keys found for ${tableName}.${columnName} or invalid response format`
1217
+ );
1218
+ // Set empty array for this field
1219
+ setJsonFieldKeys((prev) => ({
1220
+ ...prev,
1221
+ [cacheKey]: [],
1222
+ }));
1223
+ }
1224
+ } catch (error) {
1225
+ console.error(
1226
+ `Error fetching JSON field keys for ${tableName}.${columnName}:`,
1227
+ error
1228
+ );
1229
+ toast.error(
1230
+ `Failed to load JSON field keys for ${formatName(
1231
+ tableName
1232
+ )}.${formatName(columnName)}`
1233
+ );
1234
+ // Set empty array for this field on error
1235
+ setJsonFieldKeys((prev) => ({
1236
+ ...prev,
1237
+ [cacheKey]: [],
1238
+ }));
1239
+ } finally {
1240
+ // Update loading state
1241
+ setLoadingJsonKeys((prev) => ({
1242
+ ...prev,
1243
+ [cacheKey]: false,
1244
+ }));
1245
+ }
1246
+ };
1247
+
1248
+ // Check if a column is a JSON type
1249
+ const isJsonColumn = (tableName, columnName) => {
1250
+ // First check the main table
1251
+ if (tableName === selectedTable) {
1252
+ const column = tableColumns.find((col) => col.name === columnName);
1253
+ return (
1254
+ column && column.type && column.type.toLowerCase() === 'json'
1255
+ );
1256
+ }
1257
+
1258
+ // Then check joined tables
1259
+ for (const join of joins) {
1260
+ if (join.targetTable === tableName && join.availableColumns) {
1261
+ const column = join.availableColumns.find(
1262
+ (col) => col.name === columnName
1263
+ );
1264
+ return (
1265
+ column &&
1266
+ column.type &&
1267
+ column.type.toLowerCase() === 'json'
1268
+ );
1269
+ }
1270
+ }
1271
+
1272
+ return false;
1273
+ };
1274
+
1275
+ // Toggle JSON key selector visibility
1276
+ const toggleJsonKeySelector = (groupIndex, filterIndex) => {
1277
+ const key = `${groupIndex}-${filterIndex}`;
1278
+ setShowJsonKeySelector((prev) => ({
1279
+ ...prev,
1280
+ [key]: !prev[key],
1281
+ }));
1282
+ };
1283
+
1284
+ // Handle JSON key selection
1285
+ const handleJsonKeySelect = (groupIndex, filterIndex, key) => {
1286
+ // Update the filter value with the selected JSON key
1287
+ handleFilterCriterionChange(groupIndex, filterIndex, 'jsonKey', key);
1288
+
1289
+ // Hide the selector
1290
+ toggleJsonKeySelector(groupIndex, filterIndex);
1291
+ };
1292
+
1054
1293
  // Update a filter criterion
1055
1294
  const handleFilterCriterionChange = (
1056
1295
  groupIndex,
@@ -1067,6 +1306,34 @@ const GenericReport = ({ setting = {} }) => {
1067
1306
  updatedFilterCriteria.groups[groupIndex].filters[filterIndex][field] =
1068
1307
  value;
1069
1308
 
1309
+ // If we're changing the table or column, check if it's a JSON field
1310
+ if (field === 'table' || field === 'column') {
1311
+ const filter =
1312
+ updatedFilterCriteria.groups[groupIndex].filters[filterIndex];
1313
+ const tableName = field === 'table' ? value : filter.table;
1314
+ const columnName = field === 'column' ? value : filter.column;
1315
+
1316
+ // If both table and column are set, check if it's a JSON field
1317
+ if (tableName && columnName) {
1318
+ const isJson = isJsonColumn(tableName, columnName);
1319
+
1320
+ // If it's a JSON field, fetch the keys
1321
+ if (isJson) {
1322
+ fetchJsonFieldKeys(tableName, columnName);
1323
+
1324
+ // Add a jsonKey field if it doesn't exist
1325
+ if (!filter.jsonKey) {
1326
+ filter.jsonKey = '';
1327
+ }
1328
+ } else {
1329
+ // If it's not a JSON field, remove the jsonKey field if it exists
1330
+ if (filter.jsonKey) {
1331
+ delete filter.jsonKey;
1332
+ }
1333
+ }
1334
+ }
1335
+ }
1336
+
1070
1337
  setFilterCriteria(updatedFilterCriteria);
1071
1338
  };
1072
1339
 
@@ -1190,7 +1457,7 @@ const GenericReport = ({ setting = {} }) => {
1190
1457
  }
1191
1458
  };
1192
1459
 
1193
- // Export report as CSV or Excel
1460
+ // Export report as CSV, Excel or PDF
1194
1461
  const handleExportReport = async (format = 'csv') => {
1195
1462
  if (selectedColumns.length === 0) {
1196
1463
  toast.error('Please select at least one column for your report');
@@ -1202,6 +1469,15 @@ const GenericReport = ({ setting = {} }) => {
1202
1469
  return;
1203
1470
  }
1204
1471
 
1472
+ // Validate filter criteria
1473
+ if (filterCriteria) {
1474
+ const validationResult = validateFilterCriteria(filterCriteria);
1475
+ if (!validationResult.valid) {
1476
+ toast.error(validationResult.message);
1477
+ return;
1478
+ }
1479
+ }
1480
+
1205
1481
  setIsLoading(true);
1206
1482
  const toastId = toast.loading(
1207
1483
  `Exporting report as ${format.toUpperCase()}...`
@@ -1222,8 +1498,8 @@ const GenericReport = ({ setting = {} }) => {
1222
1498
  sourceColumn: join.sourceColumn,
1223
1499
  targetColumn: join.targetColumn,
1224
1500
  })),
1225
- filters: [],
1226
- sorting: [],
1501
+ filters: flattenFilterCriteria(filterCriteria),
1502
+ sorting: sortCriteria,
1227
1503
  };
1228
1504
 
1229
1505
  // Prepare export request data
@@ -1249,16 +1525,66 @@ const GenericReport = ({ setting = {} }) => {
1249
1525
  const date = moment().format('YYYYMMDD');
1250
1526
  const fileName = `${date}_${reportName}.${format}`;
1251
1527
 
1252
- // Use FileSaver.js to save the file
1253
- saveAs(res.data, fileName);
1528
+ // For PDF (HTML) format, open in a new window for printing
1529
+ if (format === 'pdf') {
1530
+ // Convert blob to URL
1531
+ const blob = new Blob([res.data], { type: 'text/html' });
1532
+ const url = URL.createObjectURL(blob);
1533
+
1534
+ // Open in a new window
1535
+ const printWindow = window.open(url, '_blank');
1536
+
1537
+ // Add print instructions
1538
+ if (printWindow) {
1539
+ printWindow.onload = function () {
1540
+ // Add print button at the top
1541
+ const printButton =
1542
+ printWindow.document.createElement('button');
1543
+ printButton.innerHTML = 'Print PDF';
1544
+ printButton.style.position = 'fixed';
1545
+ printButton.style.top = '10px';
1546
+ printButton.style.right = '10px';
1547
+ printButton.style.zIndex = '9999';
1548
+ printButton.style.padding = '8px 16px';
1549
+ printButton.style.backgroundColor = '#4CAF50';
1550
+ printButton.style.color = 'white';
1551
+ printButton.style.border = 'none';
1552
+ printButton.style.borderRadius = '4px';
1553
+ printButton.style.cursor = 'pointer';
1554
+
1555
+ printButton.onclick = function () {
1556
+ // Hide the button before printing
1557
+ this.style.display = 'none';
1558
+ printWindow.print();
1559
+ this.style.display = 'block';
1560
+ };
1254
1561
 
1255
- toast.update(toastId, {
1256
- render: `Report exported successfully as ${format.toUpperCase()}`,
1257
- type: 'success',
1258
- isLoading: false,
1259
- autoClose: 5000,
1260
- closeButton: true,
1261
- });
1562
+ printWindow.document.body.insertBefore(
1563
+ printButton,
1564
+ printWindow.document.body.firstChild
1565
+ );
1566
+ };
1567
+ }
1568
+
1569
+ toast.update(toastId, {
1570
+ render: `Report opened in a new window. Use browser's print function to save as PDF.`,
1571
+ type: 'success',
1572
+ isLoading: false,
1573
+ autoClose: 5000,
1574
+ closeButton: true,
1575
+ });
1576
+ } else {
1577
+ // Use FileSaver.js to save the file for CSV and Excel
1578
+ saveAs(res.data, fileName);
1579
+
1580
+ toast.update(toastId, {
1581
+ render: `Report exported successfully as ${format.toUpperCase()}`,
1582
+ type: 'success',
1583
+ isLoading: false,
1584
+ autoClose: 5000,
1585
+ closeButton: true,
1586
+ });
1587
+ }
1262
1588
  } else {
1263
1589
  throw new Error('Invalid response format');
1264
1590
  }
@@ -1520,6 +1846,15 @@ const GenericReport = ({ setting = {} }) => {
1520
1846
  return;
1521
1847
  }
1522
1848
 
1849
+ // Validate filter criteria
1850
+ if (filterCriteria) {
1851
+ const validationResult = validateFilterCriteria(filterCriteria);
1852
+ if (!validationResult.valid) {
1853
+ toast.error(validationResult.message);
1854
+ return;
1855
+ }
1856
+ }
1857
+
1523
1858
  setIsLoading(true);
1524
1859
  toast.info('Executing query...');
1525
1860
 
@@ -1699,6 +2034,15 @@ const GenericReport = ({ setting = {} }) => {
1699
2034
  return;
1700
2035
  }
1701
2036
 
2037
+ // Validate filter criteria
2038
+ if (filterCriteria) {
2039
+ const validationResult = validateFilterCriteria(filterCriteria);
2040
+ if (!validationResult.valid) {
2041
+ toast.error(validationResult.message);
2042
+ return;
2043
+ }
2044
+ }
2045
+
1702
2046
  setIsLoading(true);
1703
2047
 
1704
2048
  try {
@@ -2102,7 +2446,12 @@ const GenericReport = ({ setting = {} }) => {
2102
2446
  )}
2103
2447
  </div>
2104
2448
 
2105
- <h3>Selected Columns</h3>
2449
+ <div className={styles.columnSectionHeader}>
2450
+ <h3>Selected Columns</h3>
2451
+ <span className={styles.instructionText}>
2452
+ Click the pencil icon (✎) to rename columns
2453
+ </span>
2454
+ </div>
2106
2455
  {selectedColumns.length > 0 ? (
2107
2456
  <div className={styles.selectedColumns}>
2108
2457
  {selectedColumns.map((column, index) => (
@@ -2130,10 +2479,86 @@ const GenericReport = ({ setting = {} }) => {
2130
2479
  >
2131
2480
  ⋮⋮
2132
2481
  </div>
2133
- <span>{column.displayName}</span>
2482
+ <span>
2483
+ {editingColumnIndex ===
2484
+ index ? (
2485
+ <div
2486
+ className={
2487
+ styles.inlineEditContainer
2488
+ }
2489
+ >
2490
+ <input
2491
+ type="text"
2492
+ className={
2493
+ styles.columnNameInput
2494
+ }
2495
+ value={
2496
+ editingColumnName
2497
+ }
2498
+ onChange={(e) =>
2499
+ setEditingColumnName(
2500
+ e.target
2501
+ .value
2502
+ )
2503
+ }
2504
+ autoFocus
2505
+ onKeyDown={(e) => {
2506
+ if (
2507
+ e.key ===
2508
+ 'Enter'
2509
+ ) {
2510
+ handleSaveColumnName();
2511
+ } else if (
2512
+ e.key ===
2513
+ 'Escape'
2514
+ ) {
2515
+ handleCancelEditColumn();
2516
+ }
2517
+ }}
2518
+ />
2519
+ <button
2520
+ className={
2521
+ styles.saveBtn
2522
+ }
2523
+ onClick={
2524
+ handleSaveColumnName
2525
+ }
2526
+ title="Save"
2527
+ >
2528
+
2529
+ </button>
2530
+ <button
2531
+ className={
2532
+ styles.cancelBtn
2533
+ }
2534
+ onClick={
2535
+ handleCancelEditColumn
2536
+ }
2537
+ title="Cancel"
2538
+ >
2539
+
2540
+ </button>
2541
+ </div>
2542
+ ) : (
2543
+ column.alias ||
2544
+ column.displayName
2545
+ )}
2546
+ </span>
2134
2547
  <div
2135
2548
  className={styles.columnActions}
2136
2549
  >
2550
+ <button
2551
+ className={styles.editBtn}
2552
+ onClick={() =>
2553
+ handleStartEditColumn(
2554
+ index
2555
+ )
2556
+ }
2557
+ aria-label="Edit column name"
2558
+ title="Edit name"
2559
+ >
2560
+
2561
+ </button>
2137
2562
  <button
2138
2563
  className={styles.removeBtn}
2139
2564
  onClick={() =>
@@ -3615,31 +4040,218 @@ const GenericReport = ({ setting = {} }) => {
3615
4040
  styles.joinField
3616
4041
  }
3617
4042
  >
3618
- <label>
3619
- Value:
3620
- </label>
3621
- <input
3622
- type="text"
3623
- className={
3624
- styles.formControl
3625
- }
3626
- value={
3627
- criterion.value
3628
- }
3629
- onChange={(
3630
- e
3631
- ) =>
3632
- handleFilterCriterionChange(
3633
- groupIndex,
3634
- filterIndex,
3635
- 'value',
3636
- e
3637
- .target
3638
- .value
3639
- )
3640
- }
3641
- placeholder="Enter filter value"
3642
- />
4043
+ {/* Check if this is a JSON field */}
4044
+ {criterion.table &&
4045
+ criterion.column &&
4046
+ isJsonColumn(
4047
+ criterion.table,
4048
+ criterion.column
4049
+ ) ? (
4050
+ <>
4051
+ <label>
4052
+ JSON
4053
+ Path:
4054
+ </label>
4055
+ <div
4056
+ className={
4057
+ styles.jsonKeyInputContainer
4058
+ }
4059
+ >
4060
+ <input
4061
+ type="text"
4062
+ className={
4063
+ styles.formControl
4064
+ }
4065
+ value={
4066
+ criterion.jsonKey ||
4067
+ ''
4068
+ }
4069
+ onChange={(
4070
+ e
4071
+ ) =>
4072
+ handleFilterCriterionChange(
4073
+ groupIndex,
4074
+ filterIndex,
4075
+ 'jsonKey',
4076
+ e
4077
+ .target
4078
+ .value
4079
+ )
4080
+ }
4081
+ placeholder="Enter JSON path (e.g., data.name)"
4082
+ />
4083
+ <button
4084
+ type="button"
4085
+ className={
4086
+ styles.jsonKeyButton
4087
+ }
4088
+ onClick={() =>
4089
+ toggleJsonKeySelector(
4090
+ groupIndex,
4091
+ filterIndex
4092
+ )
4093
+ }
4094
+ title="Select from available JSON keys"
4095
+ >
4096
+
4097
+ </button>
4098
+ </div>
4099
+
4100
+ {/* JSON key selector dropdown */}
4101
+ {showJsonKeySelector[
4102
+ `${groupIndex}-${filterIndex}`
4103
+ ] && (
4104
+ <div
4105
+ className={
4106
+ styles.jsonKeyDropdown
4107
+ }
4108
+ >
4109
+ {loadingJsonKeys[
4110
+ `${criterion.table}.${criterion.column}`
4111
+ ] ? (
4112
+ <div
4113
+ className={
4114
+ styles.jsonKeyLoading
4115
+ }
4116
+ >
4117
+ Loading
4118
+ available
4119
+ JSON
4120
+ keys...
4121
+ </div>
4122
+ ) : jsonFieldKeys[
4123
+ `${criterion.table}.${criterion.column}`
4124
+ ]
4125
+ ?.length >
4126
+ 0 ? (
4127
+ <div
4128
+ className={
4129
+ styles.jsonKeyList
4130
+ }
4131
+ >
4132
+ {jsonFieldKeys[
4133
+ `${criterion.table}.${criterion.column}`
4134
+ ].map(
4135
+ (
4136
+ keyInfo,
4137
+ idx
4138
+ ) => (
4139
+ <div
4140
+ key={
4141
+ idx
4142
+ }
4143
+ className={
4144
+ styles.jsonKeyItem
4145
+ }
4146
+ onClick={() =>
4147
+ handleJsonKeySelect(
4148
+ groupIndex,
4149
+ filterIndex,
4150
+ keyInfo.key
4151
+ )
4152
+ }
4153
+ title={`Used in ${keyInfo.percentage}% of records. Example: ${keyInfo.example}`}
4154
+ >
4155
+ <span
4156
+ className={
4157
+ styles.jsonKeyName
4158
+ }
4159
+ >
4160
+ {
4161
+ keyInfo.key
4162
+ }
4163
+ </span>
4164
+ <span
4165
+ className={
4166
+ styles.jsonKeyFrequency
4167
+ }
4168
+ >
4169
+ {
4170
+ keyInfo.percentage
4171
+ }
4172
+
4173
+ %
4174
+ </span>
4175
+ </div>
4176
+ )
4177
+ )}
4178
+ </div>
4179
+ ) : (
4180
+ <div
4181
+ className={
4182
+ styles.jsonKeyEmpty
4183
+ }
4184
+ >
4185
+ No
4186
+ JSON
4187
+ keys
4188
+ found.
4189
+ Enter
4190
+ a
4191
+ path
4192
+ manually.
4193
+ </div>
4194
+ )}
4195
+ </div>
4196
+ )}
4197
+
4198
+ <label>
4199
+ Value:
4200
+ </label>
4201
+ <input
4202
+ type="text"
4203
+ className={
4204
+ styles.formControl
4205
+ }
4206
+ value={
4207
+ criterion.value ||
4208
+ ''
4209
+ }
4210
+ onChange={(
4211
+ e
4212
+ ) =>
4213
+ handleFilterCriterionChange(
4214
+ groupIndex,
4215
+ filterIndex,
4216
+ 'value',
4217
+ e
4218
+ .target
4219
+ .value
4220
+ )
4221
+ }
4222
+ placeholder="Enter filter value"
4223
+ />
4224
+ </>
4225
+ ) : (
4226
+ <>
4227
+ <label>
4228
+ Value:
4229
+ </label>
4230
+ <input
4231
+ type="text"
4232
+ className={
4233
+ styles.formControl
4234
+ }
4235
+ value={
4236
+ criterion.value ||
4237
+ ''
4238
+ }
4239
+ onChange={(
4240
+ e
4241
+ ) =>
4242
+ handleFilterCriterionChange(
4243
+ groupIndex,
4244
+ filterIndex,
4245
+ 'value',
4246
+ e
4247
+ .target
4248
+ .value
4249
+ )
4250
+ }
4251
+ placeholder="Enter filter value"
4252
+ />
4253
+ </>
4254
+ )}
3643
4255
  </div>
3644
4256
  )}
3645
4257
  </div>
@@ -3701,6 +4313,17 @@ const GenericReport = ({ setting = {} }) => {
3701
4313
  >
3702
4314
  Export as Excel
3703
4315
  </button>
4316
+ <button
4317
+ className={`${styles.btn} ${styles.btnSecondary}`}
4318
+ onClick={() => handleExportReport('pdf')}
4319
+ disabled={
4320
+ isLoading ||
4321
+ selectedColumns.length === 0 ||
4322
+ !reportName
4323
+ }
4324
+ >
4325
+ Export as PDF
4326
+ </button>
3704
4327
  <button
3705
4328
  className={`${styles.btn} ${styles.btnPrimary}`}
3706
4329
  onClick={handleSaveReport}
@@ -911,6 +911,55 @@
911
911
  text-overflow: ellipsis;
912
912
  white-space: nowrap;
913
913
  }
914
+
915
+ .inlineEditContainer {
916
+ display: flex;
917
+ flex: 1;
918
+ align-items: center;
919
+ gap: 4px;
920
+ width: 100%;
921
+ }
922
+
923
+ .columnNameInput {
924
+ flex: 1;
925
+ padding: 4px 8px;
926
+ border: 1px solid var(--primary-color, #2563eb);
927
+ border-radius: 4px;
928
+ font-size: 0.9em;
929
+ color: var(--paragraph-color, #4b5563);
930
+ min-width: 50px;
931
+ max-width: 200px;
932
+ }
933
+
934
+ .saveBtn,
935
+ .cancelBtn {
936
+ background: none;
937
+ border: none;
938
+ cursor: pointer;
939
+ padding: 2px 4px;
940
+ border-radius: 4px;
941
+ display: flex;
942
+ align-items: center;
943
+ justify-content: center;
944
+ transition: all 0.2s ease;
945
+ font-size: 0.9em;
946
+ min-width: 20px;
947
+ min-height: 20px;
948
+ }
949
+
950
+ .saveBtn {
951
+ color: var(--success-color, #10b981);
952
+ &:hover {
953
+ background: rgba(16, 185, 129, 0.1);
954
+ }
955
+ }
956
+
957
+ .cancelBtn {
958
+ color: var(--danger-color, #ef4444);
959
+ &:hover {
960
+ background: rgba(239, 68, 68, 0.1);
961
+ }
962
+ }
914
963
  }
915
964
 
916
965
  .selectedColumnItem:hover {
@@ -949,6 +998,25 @@
949
998
  flex-shrink: 0;
950
999
  }
951
1000
 
1001
+ .editBtn {
1002
+ background: none;
1003
+ border: none;
1004
+ color: var(--primary-color, #2563eb);
1005
+ cursor: pointer;
1006
+ font-size: 1em;
1007
+ display: flex;
1008
+ align-items: center;
1009
+ justify-content: center;
1010
+ padding: 4px;
1011
+ border-radius: 4px;
1012
+ min-width: 24px;
1013
+ min-height: 24px;
1014
+ }
1015
+
1016
+ .editBtn:hover {
1017
+ background: rgba(var(--primary-rgb, 37, 99, 235), 0.1);
1018
+ }
1019
+
952
1020
  .removeBtn {
953
1021
  background: none;
954
1022
  border: none;
@@ -1034,6 +1102,25 @@
1034
1102
  margin: 0;
1035
1103
  }
1036
1104
 
1105
+ .columnSectionHeader {
1106
+ display: flex;
1107
+ align-items: center;
1108
+ margin-bottom: 10px;
1109
+ flex-wrap: wrap;
1110
+ gap: 8px;
1111
+ }
1112
+
1113
+ .columnSectionHeader h3 {
1114
+ margin: 0;
1115
+ margin-right: 8px;
1116
+ }
1117
+
1118
+ .instructionText {
1119
+ font-size: 0.8em;
1120
+ color: var(--muted-color, #6b7280);
1121
+ font-style: italic;
1122
+ }
1123
+
1037
1124
  .btnSmall {
1038
1125
  padding: 4px 8px;
1039
1126
  font-size: 0.8em;
@@ -1157,6 +1244,94 @@
1157
1244
  margin: 16px 0;
1158
1245
  }
1159
1246
 
1247
+ .jsonKeyInputContainer {
1248
+ position: relative;
1249
+ display: flex;
1250
+ align-items: center;
1251
+ width: 100%;
1252
+
1253
+ .jsonKeyButton {
1254
+ position: absolute;
1255
+ right: 0;
1256
+ top: 0;
1257
+ height: 100%;
1258
+ background: none;
1259
+ border: none;
1260
+ border-left: 1px solid var(--border-color, #d1d5db);
1261
+ padding: 0 10px;
1262
+ color: var(--paragraph-color, #4b5563);
1263
+ cursor: pointer;
1264
+ font-size: 0.8em;
1265
+ transition: all 0.2s ease;
1266
+
1267
+ &:hover {
1268
+ background-color: #f3f4f6;
1269
+ color: var(--primary-color, #2563eb);
1270
+ }
1271
+ }
1272
+
1273
+ input {
1274
+ padding-right: 30px;
1275
+ }
1276
+ }
1277
+
1278
+ .jsonKeyDropdown {
1279
+ position: relative;
1280
+ width: 100%;
1281
+ max-height: 200px;
1282
+ overflow-y: auto;
1283
+ background-color: white;
1284
+ border: 1px solid var(--border-color, #d1d5db);
1285
+ border-radius: 6px;
1286
+ margin-top: 4px;
1287
+ margin-bottom: 12px;
1288
+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1),
1289
+ 0 2px 4px -1px rgba(0, 0, 0, 0.06);
1290
+ z-index: 100;
1291
+ }
1292
+
1293
+ .jsonKeyList {
1294
+ padding: 4px 0;
1295
+ }
1296
+
1297
+ .jsonKeyItem {
1298
+ display: flex;
1299
+ justify-content: space-between;
1300
+ align-items: center;
1301
+ padding: 8px 12px;
1302
+ cursor: pointer;
1303
+ transition: all 0.2s ease;
1304
+ font-size: 0.85em;
1305
+
1306
+ &:hover {
1307
+ background-color: #f3f4f6;
1308
+ }
1309
+
1310
+ .jsonKeyName {
1311
+ flex: 1;
1312
+ overflow: hidden;
1313
+ text-overflow: ellipsis;
1314
+ white-space: nowrap;
1315
+ color: var(--primary-color, #2563eb);
1316
+ font-weight: 500;
1317
+ }
1318
+
1319
+ .jsonKeyFrequency {
1320
+ color: var(--muted-color, #6b7280);
1321
+ font-size: 0.85em;
1322
+ margin-left: 8px;
1323
+ }
1324
+ }
1325
+
1326
+ .jsonKeyLoading,
1327
+ .jsonKeyEmpty {
1328
+ padding: 12px;
1329
+ text-align: center;
1330
+ color: var(--muted-color, #6b7280);
1331
+ font-size: 0.85em;
1332
+ font-style: italic;
1333
+ }
1334
+
1160
1335
  .jsonPreview {
1161
1336
  max-width: 100%;
1162
1337
  max-height: 150px;