@visns-studio/visns-components 6.1.8 → 6.2.1
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 +161 -1
- package/package.json +1 -1
- package/src/components/Fetch.jsx +49 -19
- package/src/components/generic/GenericFormBuilder.jsx +259 -0
- package/src/components/generic/GenericReport.jsx +1207 -286
- package/src/components/generic/SectionGroupedReport.jsx +199 -226
- package/src/components/generic/reportSemanticSteps/SemanticEntityStep.jsx +81 -0
- package/src/components/generic/reportSemanticSteps/SemanticFieldsStep.jsx +337 -0
- package/src/components/generic/reportSemanticSteps/SemanticFiltersStep.jsx +483 -0
- package/src/components/generic/reportSemanticSteps/SemanticGroupingStep.jsx +202 -0
- package/src/components/generic/reportSemanticSteps/SemanticParameterPrompt.jsx +157 -0
- package/src/components/generic/reportSemanticSteps/SemanticPreviewStep.jsx +352 -0
- package/src/components/generic/reportSemanticSteps/SemanticRelationsStep.jsx +154 -0
- package/src/components/generic/reportSemanticSteps/SemanticValueInput.jsx +153 -0
- package/src/components/generic/reportSemantics.js +971 -0
- package/src/components/generic/useSemanticModel.js +99 -0
- package/src/components/styles/ReportSemantic.module.scss +444 -0
- package/src/index.js +4 -0
|
@@ -38,6 +38,31 @@ import debugLog from '../utils/debugLog';
|
|
|
38
38
|
import { saveAs } from 'file-saver';
|
|
39
39
|
import * as XLSX from 'xlsx';
|
|
40
40
|
import Swal from 'sweetalert2';
|
|
41
|
+
import useSemanticModel from './useSemanticModel';
|
|
42
|
+
import {
|
|
43
|
+
DEFAULT_SEMANTIC_MODEL_URL,
|
|
44
|
+
appendToFilterGroup,
|
|
45
|
+
buildDefinition,
|
|
46
|
+
collectParameters,
|
|
47
|
+
createFilterCondition,
|
|
48
|
+
createFilterGroup,
|
|
49
|
+
createSelection,
|
|
50
|
+
defaultSelectionLabel,
|
|
51
|
+
emptyParameterValue,
|
|
52
|
+
getEntityLabel,
|
|
53
|
+
isSemanticDefinition,
|
|
54
|
+
parseDefinition,
|
|
55
|
+
parametersSatisfied,
|
|
56
|
+
relationPathsFromDefinition,
|
|
57
|
+
resolveFieldPath,
|
|
58
|
+
updateFilterNode,
|
|
59
|
+
} from './reportSemantics';
|
|
60
|
+
import SemanticEntityStep from './reportSemanticSteps/SemanticEntityStep';
|
|
61
|
+
import SemanticRelationsStep from './reportSemanticSteps/SemanticRelationsStep';
|
|
62
|
+
import SemanticFieldsStep from './reportSemanticSteps/SemanticFieldsStep';
|
|
63
|
+
import SemanticFiltersStep from './reportSemanticSteps/SemanticFiltersStep';
|
|
64
|
+
import SemanticGroupingStep from './reportSemanticSteps/SemanticGroupingStep';
|
|
65
|
+
import SemanticPreviewStep from './reportSemanticSteps/SemanticPreviewStep';
|
|
41
66
|
import styles from '../styles/GenericReport.module.scss';
|
|
42
67
|
import '../styles/SweetAlert.module.css';
|
|
43
68
|
import './groupedReport.css';
|
|
@@ -144,32 +169,6 @@ const getUniqueTableDisplayName = (tableName, allTables, definition) => {
|
|
|
144
169
|
return `${baseDisplayName} (${tableName})`;
|
|
145
170
|
};
|
|
146
171
|
|
|
147
|
-
// Get structured table display name data for better styling
|
|
148
|
-
const getStructuredTableDisplayName = (tableName, allTables, definition) => {
|
|
149
|
-
const baseDisplayName = getTableDisplayName(tableName, definition);
|
|
150
|
-
|
|
151
|
-
// Find all tables with the same display name
|
|
152
|
-
const conflictingTables = allTables.filter((table) => {
|
|
153
|
-
const otherTableName = typeof table === 'string' ? table : table.name;
|
|
154
|
-
const otherDisplayName = getTableDisplayName(
|
|
155
|
-
otherTableName,
|
|
156
|
-
definition
|
|
157
|
-
);
|
|
158
|
-
return (
|
|
159
|
-
otherDisplayName === baseDisplayName && otherTableName !== tableName
|
|
160
|
-
);
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
return {
|
|
164
|
-
main: baseDisplayName,
|
|
165
|
-
disambiguation: conflictingTables.length > 0 ? tableName : null,
|
|
166
|
-
full:
|
|
167
|
-
conflictingTables.length > 0
|
|
168
|
-
? `${baseDisplayName} (${tableName})`
|
|
169
|
-
: baseDisplayName,
|
|
170
|
-
};
|
|
171
|
-
};
|
|
172
|
-
|
|
173
172
|
// Get table description from definition
|
|
174
173
|
const getTableDescription = (tableName, definition) => {
|
|
175
174
|
if (definition && definition.tables) {
|
|
@@ -1013,6 +1012,110 @@ const wizardSteps = [
|
|
|
1013
1012
|
},
|
|
1014
1013
|
];
|
|
1015
1014
|
|
|
1015
|
+
/**
|
|
1016
|
+
* Wizard steps used when the semantic model is available. Same six ids and the
|
|
1017
|
+
* same order as `wizardSteps` — only the wording changes, because in this mode
|
|
1018
|
+
* the user never sees a table, a column or a join.
|
|
1019
|
+
*/
|
|
1020
|
+
const semanticWizardSteps = [
|
|
1021
|
+
{
|
|
1022
|
+
id: 'table',
|
|
1023
|
+
title: 'Choose Your Subject',
|
|
1024
|
+
icon: Data,
|
|
1025
|
+
description: 'What is this report about?',
|
|
1026
|
+
guidance: {
|
|
1027
|
+
summary: 'Pick the main thing you want to report on',
|
|
1028
|
+
howTo: [
|
|
1029
|
+
'Each card is a kind of record your system keeps',
|
|
1030
|
+
'Read the description to check it is the one you mean',
|
|
1031
|
+
'Click it to make it the subject of your report',
|
|
1032
|
+
'You can bring in connected information at the next step',
|
|
1033
|
+
],
|
|
1034
|
+
tip: 'Pick the thing you want one row per — for example one row per client.',
|
|
1035
|
+
},
|
|
1036
|
+
},
|
|
1037
|
+
{
|
|
1038
|
+
id: 'relationships',
|
|
1039
|
+
title: 'Add Related Data',
|
|
1040
|
+
icon: LinkChain,
|
|
1041
|
+
description: 'Bring in connected information (optional)',
|
|
1042
|
+
guidance: {
|
|
1043
|
+
summary: 'Add areas connected to your subject',
|
|
1044
|
+
howTo: [
|
|
1045
|
+
'Each card is an area of information already connected for you',
|
|
1046
|
+
'Click one to make its details available in the next step',
|
|
1047
|
+
'Areas marked "many per record" can repeat rows',
|
|
1048
|
+
'Skip this step if the main subject has everything you need',
|
|
1049
|
+
],
|
|
1050
|
+
tip: 'Summarising a "many per record" area (count, total) keeps one row per record.',
|
|
1051
|
+
},
|
|
1052
|
+
},
|
|
1053
|
+
{
|
|
1054
|
+
id: 'columns',
|
|
1055
|
+
title: 'Choose Information',
|
|
1056
|
+
icon: File,
|
|
1057
|
+
description: 'Pick the details you want to see',
|
|
1058
|
+
guidance: {
|
|
1059
|
+
summary: 'Tick the details to include as columns',
|
|
1060
|
+
howTo: [
|
|
1061
|
+
'Details are grouped by the area they come from',
|
|
1062
|
+
'Within each area they are sorted into kinds of information',
|
|
1063
|
+
'Tick anything you want to see in the report',
|
|
1064
|
+
'Use "Show as" to total, count or average a detail instead',
|
|
1065
|
+
],
|
|
1066
|
+
tip: 'Start with how you identify a record, then add the numbers you care about.',
|
|
1067
|
+
},
|
|
1068
|
+
},
|
|
1069
|
+
{
|
|
1070
|
+
id: 'filters',
|
|
1071
|
+
title: 'Filter Results',
|
|
1072
|
+
icon: Filter,
|
|
1073
|
+
description: 'Narrow down to specific records (optional)',
|
|
1074
|
+
guidance: {
|
|
1075
|
+
summary: 'Keep only the records you care about',
|
|
1076
|
+
howTo: [
|
|
1077
|
+
'Add a condition, then choose a detail and how to compare it',
|
|
1078
|
+
'Switch a group between "all of these" and "any of these"',
|
|
1079
|
+
'Nest a group to mix the two, e.g. active AND (email OR phone)',
|
|
1080
|
+
'Tick "ask me each time" to be prompted for the value at run time',
|
|
1081
|
+
],
|
|
1082
|
+
tip: 'Anything you might change week to week is better asked at run time than fixed here.',
|
|
1083
|
+
},
|
|
1084
|
+
},
|
|
1085
|
+
{
|
|
1086
|
+
id: 'grouping',
|
|
1087
|
+
title: 'Group & Order',
|
|
1088
|
+
icon: Data,
|
|
1089
|
+
description: 'Organise the results (optional)',
|
|
1090
|
+
guidance: {
|
|
1091
|
+
summary: 'Split the report into sections and set the row order',
|
|
1092
|
+
howTo: [
|
|
1093
|
+
'Tick a column to break the report into sections by its value',
|
|
1094
|
+
'Add a sort to control the order rows appear in',
|
|
1095
|
+
'Only columns you chose to show can be used here',
|
|
1096
|
+
'Skip this step for a plain list',
|
|
1097
|
+
],
|
|
1098
|
+
tip: 'Grouping suits reports like "clients by adviser".',
|
|
1099
|
+
},
|
|
1100
|
+
},
|
|
1101
|
+
{
|
|
1102
|
+
id: 'preview',
|
|
1103
|
+
title: 'Preview & Save',
|
|
1104
|
+
icon: EyeOpen,
|
|
1105
|
+
description: 'Review your report and save it for reuse',
|
|
1106
|
+
guidance: {
|
|
1107
|
+
summary: 'Run, review, save and export',
|
|
1108
|
+
howTo: [
|
|
1109
|
+
'Answer any run-time questions you set up',
|
|
1110
|
+
'Click "Run report" to see your data',
|
|
1111
|
+
'Save the report so you or your team can run it again',
|
|
1112
|
+
'Export the results as Excel, CSV or PDF',
|
|
1113
|
+
],
|
|
1114
|
+
tip: 'A saved report always runs against live data — save it once, run it whenever.',
|
|
1115
|
+
},
|
|
1116
|
+
},
|
|
1117
|
+
];
|
|
1118
|
+
|
|
1016
1119
|
// Icon mapping for business templates (moved from hardcoded templates)
|
|
1017
1120
|
const getTemplateIcon = (iconName) => {
|
|
1018
1121
|
const iconMap = {
|
|
@@ -1175,6 +1278,451 @@ const GenericReportImproved = ({
|
|
|
1175
1278
|
exportUrl = '/ajax/reportBuilder/export',
|
|
1176
1279
|
} = setting;
|
|
1177
1280
|
|
|
1281
|
+
// `semanticModelUrl: null` (or false) opts a host app out of the probe
|
|
1282
|
+
// entirely; omitting the key uses the contract default.
|
|
1283
|
+
const semanticModelUrl =
|
|
1284
|
+
'semanticModelUrl' in setting
|
|
1285
|
+
? setting.semanticModelUrl
|
|
1286
|
+
: DEFAULT_SEMANTIC_MODEL_URL;
|
|
1287
|
+
|
|
1288
|
+
const {
|
|
1289
|
+
model: semanticModel,
|
|
1290
|
+
status: semanticStatus,
|
|
1291
|
+
isSemantic,
|
|
1292
|
+
} = useSemanticModel(
|
|
1293
|
+
semanticModelUrl || DEFAULT_SEMANTIC_MODEL_URL,
|
|
1294
|
+
!!semanticModelUrl
|
|
1295
|
+
);
|
|
1296
|
+
|
|
1297
|
+
// ---- Semantic mode state -------------------------------------------
|
|
1298
|
+
// All of it is inert while `isSemantic` is false, which is what keeps
|
|
1299
|
+
// legacy mode byte-for-byte unchanged.
|
|
1300
|
+
const [semanticEntity, setSemanticEntity] = useState('');
|
|
1301
|
+
const [semanticRelations, setSemanticRelations] = useState([]);
|
|
1302
|
+
const [semanticSelections, setSemanticSelections] = useState([]);
|
|
1303
|
+
const [semanticFilterTree, setSemanticFilterTree] = useState(() =>
|
|
1304
|
+
createFilterGroup('and', [])
|
|
1305
|
+
);
|
|
1306
|
+
const [semanticParameters, setSemanticParameters] = useState([]);
|
|
1307
|
+
const [semanticParameterValues, setSemanticParameterValues] = useState({});
|
|
1308
|
+
const [semanticGroupBy, setSemanticGroupBy] = useState([]);
|
|
1309
|
+
const [semanticSort, setSemanticSort] = useState([]);
|
|
1310
|
+
const [semanticRows, setSemanticRows] = useState([]);
|
|
1311
|
+
const [semanticTotal, setSemanticTotal] = useState(0);
|
|
1312
|
+
const [semanticPage, setSemanticPage] = useState(1);
|
|
1313
|
+
const [semanticPageSize, setSemanticPageSize] = useState(25);
|
|
1314
|
+
const [semanticError, setSemanticError] = useState('');
|
|
1315
|
+
const [semanticHasRun, setSemanticHasRun] = useState(false);
|
|
1316
|
+
|
|
1317
|
+
const activeWizardSteps = isSemantic ? semanticWizardSteps : wizardSteps;
|
|
1318
|
+
|
|
1319
|
+
/* ---- Semantic mode handlers ---------------------------------------- */
|
|
1320
|
+
|
|
1321
|
+
const resetSemanticState = useCallback(() => {
|
|
1322
|
+
setSemanticEntity('');
|
|
1323
|
+
setSemanticRelations([]);
|
|
1324
|
+
setSemanticSelections([]);
|
|
1325
|
+
setSemanticFilterTree(createFilterGroup('and', []));
|
|
1326
|
+
setSemanticParameters([]);
|
|
1327
|
+
setSemanticParameterValues({});
|
|
1328
|
+
setSemanticGroupBy([]);
|
|
1329
|
+
setSemanticSort([]);
|
|
1330
|
+
setSemanticRows([]);
|
|
1331
|
+
setSemanticTotal(0);
|
|
1332
|
+
setSemanticPage(1);
|
|
1333
|
+
setSemanticError('');
|
|
1334
|
+
setSemanticHasRun(false);
|
|
1335
|
+
}, []);
|
|
1336
|
+
|
|
1337
|
+
const handleSemanticSelectEntity = (entityId) => {
|
|
1338
|
+
if (entityId === semanticEntity) return;
|
|
1339
|
+
// A different subject invalidates every path that was built on the
|
|
1340
|
+
// previous one, so start the semantic side of the wizard over.
|
|
1341
|
+
resetSemanticState();
|
|
1342
|
+
setSemanticEntity(entityId);
|
|
1343
|
+
};
|
|
1344
|
+
|
|
1345
|
+
const handleSemanticToggleRelation = (relationPath) => {
|
|
1346
|
+
const isRemoving = semanticRelations.includes(relationPath);
|
|
1347
|
+
|
|
1348
|
+
if (!isRemoving) {
|
|
1349
|
+
setSemanticRelations((previous) => [...previous, relationPath]);
|
|
1350
|
+
return;
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
// Removing a relation also removes anything nested under it and every
|
|
1354
|
+
// selection, filter, grouping and sort that reached through it.
|
|
1355
|
+
const prefix = `${relationPath}.`;
|
|
1356
|
+
const isOrphaned = (path) =>
|
|
1357
|
+
path === relationPath || String(path).startsWith(prefix);
|
|
1358
|
+
|
|
1359
|
+
setSemanticRelations((previous) =>
|
|
1360
|
+
previous.filter((path) => !isOrphaned(path))
|
|
1361
|
+
);
|
|
1362
|
+
setSemanticSelections((previous) =>
|
|
1363
|
+
previous.filter((selection) => !isOrphaned(selection.path))
|
|
1364
|
+
);
|
|
1365
|
+
setSemanticGroupBy((previous) =>
|
|
1366
|
+
previous.filter((path) => !isOrphaned(path))
|
|
1367
|
+
);
|
|
1368
|
+
setSemanticSort((previous) =>
|
|
1369
|
+
previous.filter((entry) => !isOrphaned(entry.field))
|
|
1370
|
+
);
|
|
1371
|
+
setSemanticFilterTree((previous) => {
|
|
1372
|
+
const prune = (node) => {
|
|
1373
|
+
if (node.kind === 'group') {
|
|
1374
|
+
return {
|
|
1375
|
+
...node,
|
|
1376
|
+
items: (node.items || [])
|
|
1377
|
+
.map(prune)
|
|
1378
|
+
.filter((child) => child !== null),
|
|
1379
|
+
};
|
|
1380
|
+
}
|
|
1381
|
+
return isOrphaned(node.field) ? null : node;
|
|
1382
|
+
};
|
|
1383
|
+
return prune(previous);
|
|
1384
|
+
});
|
|
1385
|
+
};
|
|
1386
|
+
|
|
1387
|
+
const handleSemanticToggleField = (path) => {
|
|
1388
|
+
setSemanticSelections((previous) => {
|
|
1389
|
+
const exists = previous.some(
|
|
1390
|
+
(selection) => selection.path === path
|
|
1391
|
+
);
|
|
1392
|
+
if (exists) {
|
|
1393
|
+
return previous.filter(
|
|
1394
|
+
(selection) => selection.path !== path
|
|
1395
|
+
);
|
|
1396
|
+
}
|
|
1397
|
+
const selection = createSelection(
|
|
1398
|
+
semanticModel,
|
|
1399
|
+
semanticEntity,
|
|
1400
|
+
path
|
|
1401
|
+
);
|
|
1402
|
+
return selection ? [...previous, selection] : previous;
|
|
1403
|
+
});
|
|
1404
|
+
};
|
|
1405
|
+
|
|
1406
|
+
const handleSemanticChangeAggregate = (path, agg) => {
|
|
1407
|
+
setSemanticSelections((previous) =>
|
|
1408
|
+
previous.map((selection) => {
|
|
1409
|
+
if (selection.path !== path) return selection;
|
|
1410
|
+
const resolved = resolveFieldPath(
|
|
1411
|
+
semanticModel,
|
|
1412
|
+
semanticEntity,
|
|
1413
|
+
path
|
|
1414
|
+
);
|
|
1415
|
+
if (!resolved) return selection;
|
|
1416
|
+
return {
|
|
1417
|
+
...selection,
|
|
1418
|
+
agg: agg || '',
|
|
1419
|
+
label: defaultSelectionLabel(resolved, agg),
|
|
1420
|
+
};
|
|
1421
|
+
})
|
|
1422
|
+
);
|
|
1423
|
+
|
|
1424
|
+
// An aggregate cannot also be the grouping key.
|
|
1425
|
+
if (agg) {
|
|
1426
|
+
setSemanticGroupBy((previous) =>
|
|
1427
|
+
previous.filter((entry) => entry !== path)
|
|
1428
|
+
);
|
|
1429
|
+
}
|
|
1430
|
+
};
|
|
1431
|
+
|
|
1432
|
+
const handleSemanticRemoveSelection = (path, agg) => {
|
|
1433
|
+
setSemanticSelections((previous) =>
|
|
1434
|
+
previous.filter(
|
|
1435
|
+
(selection) =>
|
|
1436
|
+
!(
|
|
1437
|
+
selection.path === path &&
|
|
1438
|
+
(selection.agg || '') === (agg || '')
|
|
1439
|
+
)
|
|
1440
|
+
)
|
|
1441
|
+
);
|
|
1442
|
+
setSemanticGroupBy((previous) =>
|
|
1443
|
+
previous.filter((entry) => entry !== path)
|
|
1444
|
+
);
|
|
1445
|
+
setSemanticSort((previous) =>
|
|
1446
|
+
previous.filter((entry) => entry.field !== path)
|
|
1447
|
+
);
|
|
1448
|
+
};
|
|
1449
|
+
|
|
1450
|
+
const handleSemanticMoveSelection = (from, to) => {
|
|
1451
|
+
setSemanticSelections((previous) => {
|
|
1452
|
+
if (to < 0 || to >= previous.length) return previous;
|
|
1453
|
+
const next = [...previous];
|
|
1454
|
+
const [moved] = next.splice(from, 1);
|
|
1455
|
+
next.splice(to, 0, moved);
|
|
1456
|
+
return next;
|
|
1457
|
+
});
|
|
1458
|
+
};
|
|
1459
|
+
|
|
1460
|
+
const handleSemanticChangeFilterNode = (nodeId, patch) => {
|
|
1461
|
+
setSemanticFilterTree((previous) =>
|
|
1462
|
+
updateFilterNode(previous, nodeId, (node) => ({
|
|
1463
|
+
...node,
|
|
1464
|
+
...patch,
|
|
1465
|
+
}))
|
|
1466
|
+
);
|
|
1467
|
+
};
|
|
1468
|
+
|
|
1469
|
+
const handleSemanticRemoveFilterNode = (nodeId) => {
|
|
1470
|
+
setSemanticFilterTree((previous) =>
|
|
1471
|
+
updateFilterNode(previous, nodeId, () => null)
|
|
1472
|
+
);
|
|
1473
|
+
};
|
|
1474
|
+
|
|
1475
|
+
const handleSemanticAddFilterCondition = (groupId) => {
|
|
1476
|
+
setSemanticFilterTree((previous) =>
|
|
1477
|
+
appendToFilterGroup(previous, groupId, createFilterCondition())
|
|
1478
|
+
);
|
|
1479
|
+
};
|
|
1480
|
+
|
|
1481
|
+
const handleSemanticAddFilterGroup = (groupId) => {
|
|
1482
|
+
setSemanticFilterTree((previous) =>
|
|
1483
|
+
appendToFilterGroup(
|
|
1484
|
+
previous,
|
|
1485
|
+
groupId,
|
|
1486
|
+
createFilterGroup('or', [createFilterCondition()])
|
|
1487
|
+
)
|
|
1488
|
+
);
|
|
1489
|
+
};
|
|
1490
|
+
|
|
1491
|
+
const handleSemanticChangeParameter = (parameterId, patch) => {
|
|
1492
|
+
setSemanticParameters((previous) =>
|
|
1493
|
+
previous.map((parameter) =>
|
|
1494
|
+
parameter.id === parameterId
|
|
1495
|
+
? { ...parameter, ...patch }
|
|
1496
|
+
: parameter
|
|
1497
|
+
)
|
|
1498
|
+
);
|
|
1499
|
+
};
|
|
1500
|
+
|
|
1501
|
+
const handleSemanticChangeParameterValue = (parameterId, value) => {
|
|
1502
|
+
setSemanticParameterValues((previous) => ({
|
|
1503
|
+
...previous,
|
|
1504
|
+
[parameterId]: value,
|
|
1505
|
+
}));
|
|
1506
|
+
};
|
|
1507
|
+
|
|
1508
|
+
const handleSemanticToggleGroupBy = (path) => {
|
|
1509
|
+
setSemanticGroupBy((previous) =>
|
|
1510
|
+
previous.includes(path)
|
|
1511
|
+
? previous.filter((entry) => entry !== path)
|
|
1512
|
+
: [...previous, path]
|
|
1513
|
+
);
|
|
1514
|
+
};
|
|
1515
|
+
|
|
1516
|
+
const handleSemanticAddSort = (field, dir) => {
|
|
1517
|
+
setSemanticSort((previous) => [...previous, { field, dir }]);
|
|
1518
|
+
};
|
|
1519
|
+
|
|
1520
|
+
const handleSemanticChangeSort = (index, patch) => {
|
|
1521
|
+
setSemanticSort((previous) =>
|
|
1522
|
+
previous.map((entry, entryIndex) =>
|
|
1523
|
+
entryIndex === index ? { ...entry, ...patch } : entry
|
|
1524
|
+
)
|
|
1525
|
+
);
|
|
1526
|
+
};
|
|
1527
|
+
|
|
1528
|
+
const handleSemanticRemoveSort = (index) => {
|
|
1529
|
+
setSemanticSort((previous) =>
|
|
1530
|
+
previous.filter((entry, entryIndex) => entryIndex !== index)
|
|
1531
|
+
);
|
|
1532
|
+
};
|
|
1533
|
+
|
|
1534
|
+
// Parameter definitions are derived from the filter tree: ticking "ask me
|
|
1535
|
+
// each time" creates one, unticking removes it. Edited labels survive.
|
|
1536
|
+
useEffect(() => {
|
|
1537
|
+
if (!isSemantic || !semanticEntity) return;
|
|
1538
|
+
setSemanticParameters((previous) => {
|
|
1539
|
+
const next = collectParameters(
|
|
1540
|
+
semanticModel,
|
|
1541
|
+
semanticEntity,
|
|
1542
|
+
semanticFilterTree,
|
|
1543
|
+
previous
|
|
1544
|
+
);
|
|
1545
|
+
const unchanged =
|
|
1546
|
+
next.length === previous.length &&
|
|
1547
|
+
next.every(
|
|
1548
|
+
(parameter, index) =>
|
|
1549
|
+
parameter.id === previous[index].id &&
|
|
1550
|
+
parameter.label === previous[index].label &&
|
|
1551
|
+
parameter.type === previous[index].type &&
|
|
1552
|
+
parameter.required === previous[index].required
|
|
1553
|
+
);
|
|
1554
|
+
return unchanged ? previous : next;
|
|
1555
|
+
});
|
|
1556
|
+
}, [isSemantic, semanticModel, semanticEntity, semanticFilterTree]);
|
|
1557
|
+
|
|
1558
|
+
// Seed a blank answer for each parameter so the inputs stay controlled.
|
|
1559
|
+
useEffect(() => {
|
|
1560
|
+
if (!isSemantic) return;
|
|
1561
|
+
setSemanticParameterValues((previous) => {
|
|
1562
|
+
const next = {};
|
|
1563
|
+
semanticParameters.forEach((parameter) => {
|
|
1564
|
+
next[parameter.id] =
|
|
1565
|
+
previous[parameter.id] ??
|
|
1566
|
+
emptyParameterValue(parameter.type);
|
|
1567
|
+
});
|
|
1568
|
+
return next;
|
|
1569
|
+
});
|
|
1570
|
+
}, [isSemantic, semanticParameters]);
|
|
1571
|
+
|
|
1572
|
+
/** The v2 document this wizard currently describes. */
|
|
1573
|
+
const semanticDefinition = useMemo(() => {
|
|
1574
|
+
if (!isSemantic || !semanticEntity) return null;
|
|
1575
|
+
return buildDefinition({
|
|
1576
|
+
model: semanticModel,
|
|
1577
|
+
entity: semanticEntity,
|
|
1578
|
+
selections: semanticSelections,
|
|
1579
|
+
filterTree: semanticFilterTree,
|
|
1580
|
+
parameters: semanticParameters,
|
|
1581
|
+
groupBy: semanticGroupBy,
|
|
1582
|
+
sort: semanticSort,
|
|
1583
|
+
});
|
|
1584
|
+
}, [
|
|
1585
|
+
isSemantic,
|
|
1586
|
+
semanticModel,
|
|
1587
|
+
semanticEntity,
|
|
1588
|
+
semanticSelections,
|
|
1589
|
+
semanticFilterTree,
|
|
1590
|
+
semanticParameters,
|
|
1591
|
+
semanticGroupBy,
|
|
1592
|
+
semanticSort,
|
|
1593
|
+
]);
|
|
1594
|
+
|
|
1595
|
+
/**
|
|
1596
|
+
* The server's own words for a rejected definition.
|
|
1597
|
+
*
|
|
1598
|
+
* A bad definition comes back as HTTP 422 with
|
|
1599
|
+
* `{success:false, message, errors:[{path, message}]}` — but a non-2xx
|
|
1600
|
+
* response makes CustomFetch reject rather than resolve, so the body never
|
|
1601
|
+
* reaches the `success === false` branch below. "Unknown field [x] on
|
|
1602
|
+
* [clients]" is exactly what the user needs to fix the report, so dig it
|
|
1603
|
+
* off the error before falling back to a generic line.
|
|
1604
|
+
*/
|
|
1605
|
+
const semanticErrorMessage = (error, fallback) => {
|
|
1606
|
+
const data = error?.response?.data;
|
|
1607
|
+
if (typeof data?.message === 'string' && data.message) {
|
|
1608
|
+
return data.message;
|
|
1609
|
+
}
|
|
1610
|
+
const first = Array.isArray(data?.errors) ? data.errors[0] : null;
|
|
1611
|
+
if (typeof first?.message === 'string' && first.message) {
|
|
1612
|
+
return first.message;
|
|
1613
|
+
}
|
|
1614
|
+
return fallback;
|
|
1615
|
+
};
|
|
1616
|
+
|
|
1617
|
+
/**
|
|
1618
|
+
* POST executeUrl {definition, parameters, limit, offset}
|
|
1619
|
+
* -> {success, data: [rows], total}
|
|
1620
|
+
*/
|
|
1621
|
+
const runSemanticReport = async ({ page, size } = {}) => {
|
|
1622
|
+
if (!semanticDefinition || semanticSelections.length === 0) return;
|
|
1623
|
+
|
|
1624
|
+
const pageToLoad = page || semanticPage;
|
|
1625
|
+
const sizeToLoad = size || semanticPageSize;
|
|
1626
|
+
|
|
1627
|
+
if (!parametersSatisfied(semanticParameters, semanticParameterValues)) {
|
|
1628
|
+
setSemanticError(
|
|
1629
|
+
'Answer every required question before running this report.'
|
|
1630
|
+
);
|
|
1631
|
+
return;
|
|
1632
|
+
}
|
|
1633
|
+
|
|
1634
|
+
setIsLoading(true);
|
|
1635
|
+
setSemanticError('');
|
|
1636
|
+
|
|
1637
|
+
try {
|
|
1638
|
+
const result = await CustomFetch(
|
|
1639
|
+
customUrls.executeUrl || executeUrl,
|
|
1640
|
+
'POST',
|
|
1641
|
+
{
|
|
1642
|
+
definition: semanticDefinition,
|
|
1643
|
+
parameters: semanticParameterValues,
|
|
1644
|
+
limit: sizeToLoad,
|
|
1645
|
+
offset: (pageToLoad - 1) * sizeToLoad,
|
|
1646
|
+
},
|
|
1647
|
+
null,
|
|
1648
|
+
() => {},
|
|
1649
|
+
{ cache: false }
|
|
1650
|
+
);
|
|
1651
|
+
|
|
1652
|
+
const body = result?.data;
|
|
1653
|
+
|
|
1654
|
+
if (body?.success === false) {
|
|
1655
|
+
setSemanticRows([]);
|
|
1656
|
+
setSemanticTotal(0);
|
|
1657
|
+
setSemanticError(
|
|
1658
|
+
body.message || 'The report could not be run.'
|
|
1659
|
+
);
|
|
1660
|
+
return;
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1663
|
+
const rows = Array.isArray(body?.data) ? body.data : [];
|
|
1664
|
+
setSemanticRows(rows);
|
|
1665
|
+
setSemanticTotal(
|
|
1666
|
+
typeof body?.total === 'number' ? body.total : rows.length
|
|
1667
|
+
);
|
|
1668
|
+
setSemanticPage(pageToLoad);
|
|
1669
|
+
setSemanticPageSize(sizeToLoad);
|
|
1670
|
+
setSemanticHasRun(true);
|
|
1671
|
+
} catch (error) {
|
|
1672
|
+
debugLog('Semantic report execution failed:', error);
|
|
1673
|
+
setSemanticRows([]);
|
|
1674
|
+
setSemanticTotal(0);
|
|
1675
|
+
setSemanticError(
|
|
1676
|
+
semanticErrorMessage(
|
|
1677
|
+
error,
|
|
1678
|
+
'We could not run this report. Please try again.'
|
|
1679
|
+
)
|
|
1680
|
+
);
|
|
1681
|
+
} finally {
|
|
1682
|
+
setIsLoading(false);
|
|
1683
|
+
}
|
|
1684
|
+
};
|
|
1685
|
+
|
|
1686
|
+
/** POST exportUrl {definition, parameters, format} -> blob */
|
|
1687
|
+
const exportSemanticReport = async (format) => {
|
|
1688
|
+
if (!semanticDefinition) return;
|
|
1689
|
+
|
|
1690
|
+
try {
|
|
1691
|
+
const result = await Download(
|
|
1692
|
+
customUrls.exportUrl || exportUrl,
|
|
1693
|
+
'POST',
|
|
1694
|
+
{
|
|
1695
|
+
definition: semanticDefinition,
|
|
1696
|
+
parameters: semanticParameterValues,
|
|
1697
|
+
format,
|
|
1698
|
+
}
|
|
1699
|
+
);
|
|
1700
|
+
|
|
1701
|
+
if (result?.data) {
|
|
1702
|
+
const timestamp = moment().format('YYYY-MM-DD_HH-mm-ss');
|
|
1703
|
+
const baseName = (
|
|
1704
|
+
reportName ||
|
|
1705
|
+
getEntityLabel(semanticModel, semanticEntity) ||
|
|
1706
|
+
'report'
|
|
1707
|
+
).replace(/[^a-zA-Z0-9]/g, '_');
|
|
1708
|
+
saveAs(result.data, `${baseName}_${timestamp}.${format}`);
|
|
1709
|
+
toast.success(`Report exported as ${format.toUpperCase()}`);
|
|
1710
|
+
} else {
|
|
1711
|
+
toast.error(
|
|
1712
|
+
`Failed to export report as ${format.toUpperCase()}`
|
|
1713
|
+
);
|
|
1714
|
+
}
|
|
1715
|
+
} catch (error) {
|
|
1716
|
+
debugLog('Semantic export failed:', error);
|
|
1717
|
+
toast.error(
|
|
1718
|
+
semanticErrorMessage(
|
|
1719
|
+
error,
|
|
1720
|
+
`Failed to export report as ${format.toUpperCase()}`
|
|
1721
|
+
)
|
|
1722
|
+
);
|
|
1723
|
+
}
|
|
1724
|
+
};
|
|
1725
|
+
|
|
1178
1726
|
// Simple icon helper for SweetAlert2 - returns HTML strings with SVG icons
|
|
1179
1727
|
const getIcon = (iconName, color = '#6b7280', size = 16) => {
|
|
1180
1728
|
const icons = {
|
|
@@ -1204,224 +1752,56 @@ const GenericReportImproved = ({
|
|
|
1204
1752
|
return icons[iconName] || icons.chart;
|
|
1205
1753
|
};
|
|
1206
1754
|
|
|
1207
|
-
|
|
1208
|
-
|
|
1755
|
+
/* ---------------------------------------------------------------- */
|
|
1756
|
+
/* Guided help */
|
|
1757
|
+
/* ---------------------------------------------------------------- */
|
|
1758
|
+
|
|
1759
|
+
/**
|
|
1760
|
+
* One heading style for both guides, so the legacy and semantic help read
|
|
1761
|
+
* as the same help system. `icon` is a key of `getIcon`.
|
|
1762
|
+
*/
|
|
1763
|
+
const helpHeading = (icon, color, title) =>
|
|
1764
|
+
`<h3 style="color: #1f2937; margin: 0 0 12px 0; font-size: 16px; border-bottom: 2px solid ${color}; padding-bottom: 6px; display: flex; align-items: center; gap: 8px;">${getIcon(
|
|
1765
|
+
icon,
|
|
1766
|
+
color,
|
|
1767
|
+
16
|
|
1768
|
+
)}${title}</h3>`;
|
|
1769
|
+
|
|
1770
|
+
/**
|
|
1771
|
+
* A tinted panel with a bulleted body. Passing `items` as a string renders
|
|
1772
|
+
* it as a paragraph instead, for the short explanatory panels.
|
|
1773
|
+
*/
|
|
1774
|
+
const helpPanel = ({ bg, border, text, title, icon, items }) => {
|
|
1775
|
+
const body = Array.isArray(items)
|
|
1776
|
+
? `<ul style="margin: 0; padding-left: 16px; color: ${text};">${items
|
|
1777
|
+
.map((item) => `<li>${item}</li>`)
|
|
1778
|
+
.join('')}</ul>`
|
|
1779
|
+
: `<p style="margin: 0; color: ${text};">${items}</p>`;
|
|
1780
|
+
|
|
1781
|
+
return `
|
|
1782
|
+
<div style="background: ${bg}; border: 1px solid ${border}; border-radius: 8px; padding: 12px; margin-bottom: 12px;">
|
|
1783
|
+
${
|
|
1784
|
+
title
|
|
1785
|
+
? `<h4 style="color: ${border}; margin: 0 0 8px 0; font-size: 14px; display: flex; align-items: center; gap: 6px;">${
|
|
1786
|
+
icon ? getIcon(icon, border, 14) : ''
|
|
1787
|
+
}${title}</h4>`
|
|
1788
|
+
: ''
|
|
1789
|
+
}
|
|
1790
|
+
${body}
|
|
1791
|
+
</div>`;
|
|
1792
|
+
};
|
|
1793
|
+
|
|
1794
|
+
/** Shared SweetAlert2 options so both guides open the same way. */
|
|
1795
|
+
const showHelpModal = (titleIcon, titleText, sections) =>
|
|
1209
1796
|
Swal.fire({
|
|
1210
1797
|
title: `<div style="display: flex; align-items: center; justify-content: center; gap: 8px;">${getIcon(
|
|
1211
|
-
|
|
1798
|
+
titleIcon,
|
|
1212
1799
|
'#3b82f6',
|
|
1213
1800
|
24
|
|
1214
|
-
)}
|
|
1215
|
-
html:
|
|
1216
|
-
<div style="
|
|
1217
|
-
|
|
1218
|
-
<!-- Quick Start Section -->
|
|
1219
|
-
<div style="margin-bottom: 24px;">
|
|
1220
|
-
<h3 style="color: #1f2937; margin: 0 0 12px 0; font-size: 16px; border-bottom: 2px solid #3b82f6; padding-bottom: 6px; display: flex; align-items: center; gap: 8px;">
|
|
1221
|
-
${getIcon('rocket', '#3b82f6', 16)}
|
|
1222
|
-
Getting Started
|
|
1223
|
-
</h3>
|
|
1224
|
-
|
|
1225
|
-
<div style="background: #f0f9ff; border-left: 4px solid #3b82f6; padding: 12px; margin-bottom: 12px;">
|
|
1226
|
-
<h4 style="color: #1e40af; margin: 0 0 8px 0; font-size: 14px; display: flex; align-items: center; gap: 6px;">
|
|
1227
|
-
${getIcon('file', '#1e40af', 14)}
|
|
1228
|
-
Option 1: Use Saved Reports (Recommended)
|
|
1229
|
-
</h4>
|
|
1230
|
-
<p style="margin: 0 0 8px 0;">Click the "Templates" button to access pre-built reports for common needs like client lists, recent orders, or financial summaries.</p>
|
|
1231
|
-
<p style="margin: 0; font-style: italic; color: #1e40af;">Perfect for beginners and quick results!</p>
|
|
1232
|
-
</div>
|
|
1233
|
-
|
|
1234
|
-
<div style="background: #f9fafb; border-left: 4px solid #6b7280; padding: 12px;">
|
|
1235
|
-
<h4 style="color: #374151; margin: 0 0 8px 0; font-size: 14px; display: flex; align-items: center; gap: 6px;">
|
|
1236
|
-
${getIcon('cogs', '#374151', 14)}
|
|
1237
|
-
Option 2: Build Custom Report
|
|
1238
|
-
</h4>
|
|
1239
|
-
<p style="margin: 0;">Follow the 5-step wizard below to create exactly what you need.</p>
|
|
1240
|
-
</div>
|
|
1241
|
-
</div>
|
|
1242
|
-
|
|
1243
|
-
<!-- Step-by-Step Guide -->
|
|
1244
|
-
<div style="margin-bottom: 24px;">
|
|
1245
|
-
<h3 style="color: #1f2937; margin: 0 0 12px 0; font-size: 16px; border-bottom: 2px solid #10b981; padding-bottom: 6px; display: flex; align-items: center; gap: 8px;">
|
|
1246
|
-
${getIcon('list', '#10b981', 16)}
|
|
1247
|
-
5-Step Report Creation
|
|
1248
|
-
</h3>
|
|
1249
|
-
|
|
1250
|
-
<!-- Step 1 -->
|
|
1251
|
-
<div style="background: #ecfdf5; border: 1px solid #10b981; border-radius: 8px; padding: 12px; margin-bottom: 12px;">
|
|
1252
|
-
<h4 style="color: #047857; margin: 0 0 8px 0; font-size: 14px; display: flex; align-items: center; gap: 6px;">
|
|
1253
|
-
${getIcon('database', '#047857', 14)}
|
|
1254
|
-
Step 1: Choose Your Data Source
|
|
1255
|
-
</h4>
|
|
1256
|
-
<ul style="margin: 0; padding-left: 16px; color: #065f46;">
|
|
1257
|
-
<li>Select the main table (e.g., Clients, Orders, Invoices)</li>
|
|
1258
|
-
<li>This becomes the foundation of your report</li>
|
|
1259
|
-
<li>Look for the table icon to identify data types</li>
|
|
1260
|
-
</ul>
|
|
1261
|
-
</div>
|
|
1262
|
-
|
|
1263
|
-
<!-- Step 2 -->
|
|
1264
|
-
<div style="background: #eff6ff; border: 1px solid #3b82f6; border-radius: 8px; padding: 12px; margin-bottom: 12px;">
|
|
1265
|
-
<h4 style="color: #1e40af; margin: 0 0 8px 0; font-size: 14px; display: flex; align-items: center; gap: 6px;">
|
|
1266
|
-
${getIcon('columns', '#1e40af', 14)}
|
|
1267
|
-
Step 2: Select Columns
|
|
1268
|
-
</h4>
|
|
1269
|
-
<ul style="margin: 0; padding-left: 16px; color: #1e3a8a;">
|
|
1270
|
-
<li>Check the boxes for data you want to see</li>
|
|
1271
|
-
<li>Drag columns to reorder them</li>
|
|
1272
|
-
<li>Use "Select All" for comprehensive reports</li>
|
|
1273
|
-
<li>Preview shows how your data will look</li>
|
|
1274
|
-
</ul>
|
|
1275
|
-
</div>
|
|
1276
|
-
|
|
1277
|
-
<!-- Step 3 -->
|
|
1278
|
-
<div style="background: #fef3c7; border: 1px solid #f59e0b; border-radius: 8px; padding: 12px; margin-bottom: 12px;">
|
|
1279
|
-
<h4 style="color: #92400e; margin: 0 0 8px 0; font-size: 14px; display: flex; align-items: center; gap: 6px;">
|
|
1280
|
-
${getIcon('link', '#92400e', 14)}
|
|
1281
|
-
Step 3: Add Relationships (Optional)
|
|
1282
|
-
</h4>
|
|
1283
|
-
<ul style="margin: 0; padding-left: 16px; color: #92400e;">
|
|
1284
|
-
<li>Connect related data (e.g., add customer names to invoices)</li>
|
|
1285
|
-
<li>We suggest common relationships automatically</li>
|
|
1286
|
-
<li>Click "Add Relationship" to connect more tables</li>
|
|
1287
|
-
<li>This enriches your report with additional context</li>
|
|
1288
|
-
</ul>
|
|
1289
|
-
</div>
|
|
1290
|
-
|
|
1291
|
-
<!-- Step 4 -->
|
|
1292
|
-
<div style="background: #fdf2f8; border: 1px solid #ec4899; border-radius: 8px; padding: 12px; margin-bottom: 12px;">
|
|
1293
|
-
<h4 style="color: #be185d; margin: 0 0 8px 0; font-size: 14px; display: flex; align-items: center; gap: 6px;">
|
|
1294
|
-
${getIcon('filter', '#be185d', 14)}
|
|
1295
|
-
Step 4: Apply Filters (Optional)
|
|
1296
|
-
</h4>
|
|
1297
|
-
<ul style="margin: 0; padding-left: 16px; color: #be185d;">
|
|
1298
|
-
<li>Narrow down results (e.g., "Active clients only")</li>
|
|
1299
|
-
<li>Use AND/OR logic for complex conditions</li>
|
|
1300
|
-
<li>Group filters for advanced filtering</li>
|
|
1301
|
-
<li>Great for date ranges, status filters, etc.</li>
|
|
1302
|
-
</ul>
|
|
1303
|
-
</div>
|
|
1304
|
-
|
|
1305
|
-
<!-- Step 5 -->
|
|
1306
|
-
<div style="background: #f3e8ff; border: 1px solid #8b5cf6; border-radius: 8px; padding: 12px;">
|
|
1307
|
-
<h4 style="color: #7c3aed; margin: 0 0 8px 0; font-size: 14px; display: flex; align-items: center; gap: 6px;">
|
|
1308
|
-
${getIcon('download', '#7c3aed', 14)}
|
|
1309
|
-
Step 5: Generate & Export
|
|
1310
|
-
</h4>
|
|
1311
|
-
<ul style="margin: 0; padding-left: 16px; color: #7c3aed;">
|
|
1312
|
-
<li>Click "Generate Report" to see your data</li>
|
|
1313
|
-
<li>Review and refine as needed</li>
|
|
1314
|
-
<li>Export as Excel, CSV, or PDF</li>
|
|
1315
|
-
<li>Save the report template for future use</li>
|
|
1316
|
-
</ul>
|
|
1317
|
-
</div>
|
|
1318
|
-
</div>
|
|
1319
|
-
|
|
1320
|
-
<!-- Pro Tips Section -->
|
|
1321
|
-
<div style="margin-bottom: 20px;">
|
|
1322
|
-
<h3 style="color: #1f2937; margin: 0 0 12px 0; font-size: 16px; border-bottom: 2px solid #f59e0b; padding-bottom: 6px; display: flex; align-items: center; gap: 8px;">
|
|
1323
|
-
${getIcon('star', '#f59e0b', 16)}
|
|
1324
|
-
Pro Tips
|
|
1325
|
-
</h3>
|
|
1326
|
-
|
|
1327
|
-
<div style="background: #fffbeb; border: 1px solid #f59e0b; border-radius: 8px; padding: 12px;">
|
|
1328
|
-
<ul style="margin: 0; padding-left: 16px; color: #92400e;">
|
|
1329
|
-
<li><strong>Start Simple:</strong> Begin with basic reports, then add complexity</li>
|
|
1330
|
-
<li><strong>Use Previews:</strong> Check the preview before generating large reports</li>
|
|
1331
|
-
<li><strong>Save Templates:</strong> Save frequently used reports as templates</li>
|
|
1332
|
-
<li><strong>Date Filters:</strong> Use date ranges for better performance</li>
|
|
1333
|
-
<li><strong>Column Limits:</strong> Too many columns can slow down reports</li>
|
|
1334
|
-
</ul>
|
|
1335
|
-
</div>
|
|
1336
|
-
</div>
|
|
1337
|
-
|
|
1338
|
-
<!-- Common Use Cases -->
|
|
1339
|
-
<div style="margin-bottom: 20px;">
|
|
1340
|
-
<h3 style="color: #1f2937; margin: 0 0 12px 0; font-size: 16px; border-bottom: 2px solid #10b981; padding-bottom: 6px; display: flex; align-items: center; gap: 8px;">
|
|
1341
|
-
${getIcon('lightbulb', '#10b981', 16)}
|
|
1342
|
-
Common Report Examples
|
|
1343
|
-
</h3>
|
|
1344
|
-
|
|
1345
|
-
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 8px;">
|
|
1346
|
-
<div style="background: #f0fdf4; border: 1px solid #22c55e; border-radius: 6px; padding: 8px;">
|
|
1347
|
-
<strong style="color: #15803d;">${getIcon(
|
|
1348
|
-
'users',
|
|
1349
|
-
'#15803d',
|
|
1350
|
-
12
|
|
1351
|
-
)}Client List:</strong>
|
|
1352
|
-
<br><small style="color: #16a34a;">Clients table + Contact details</small>
|
|
1353
|
-
</div>
|
|
1354
|
-
<div style="background: #eff6ff; border: 1px solid #3b82f6; border-radius: 6px; padding: 8px;">
|
|
1355
|
-
<strong style="color: #1d4ed8;">${getIcon(
|
|
1356
|
-
'cart',
|
|
1357
|
-
'#1d4ed8',
|
|
1358
|
-
12
|
|
1359
|
-
)}Recent Orders:</strong>
|
|
1360
|
-
<br><small style="color: #2563eb;">Orders + Customer names + Date filter</small>
|
|
1361
|
-
</div>
|
|
1362
|
-
<div style="background: #fef3c7; border: 1px solid #f59e0b; border-radius: 6px; padding: 8px;">
|
|
1363
|
-
<strong style="color: #d97706;">${getIcon(
|
|
1364
|
-
'money',
|
|
1365
|
-
'#d97706',
|
|
1366
|
-
12
|
|
1367
|
-
)}Financial Summary:</strong>
|
|
1368
|
-
<br><small style="color: #f59e0b;">Invoices + Payments + Date grouping</small>
|
|
1369
|
-
</div>
|
|
1370
|
-
<div style="background: #fdf2f8; border: 1px solid #ec4899; border-radius: 6px; padding: 8px;">
|
|
1371
|
-
<strong style="color: #db2777;">${getIcon(
|
|
1372
|
-
'tasks',
|
|
1373
|
-
'#db2777',
|
|
1374
|
-
12
|
|
1375
|
-
)}Active Projects:</strong>
|
|
1376
|
-
<br><small style="color: #ec4899;">Projects + Status filter + Team members</small>
|
|
1377
|
-
</div>
|
|
1378
|
-
</div>
|
|
1379
|
-
</div>
|
|
1380
|
-
|
|
1381
|
-
<!-- Troubleshooting -->
|
|
1382
|
-
<div>
|
|
1383
|
-
<h3 style="color: #1f2937; margin: 0 0 12px 0; font-size: 16px; border-bottom: 2px solid #dc2626; padding-bottom: 6px; display: flex; align-items: center; gap: 8px;">
|
|
1384
|
-
${getIcon('settings', '#dc2626', 16)}
|
|
1385
|
-
Common Issues
|
|
1386
|
-
</h3>
|
|
1387
|
-
|
|
1388
|
-
<div style="background: #fef2f2; border: 1px solid #dc2626; border-radius: 8px; padding: 12px;">
|
|
1389
|
-
<div style="margin-bottom: 8px;">
|
|
1390
|
-
<strong style="color: #dc2626;">${getIcon(
|
|
1391
|
-
'clock',
|
|
1392
|
-
'#dc2626',
|
|
1393
|
-
12
|
|
1394
|
-
)}Report too slow?</strong>
|
|
1395
|
-
<span style="color: #991b1b;"> Add date filters or reduce columns</span>
|
|
1396
|
-
</div>
|
|
1397
|
-
<div style="margin-bottom: 8px;">
|
|
1398
|
-
<strong style="color: #dc2626;">${getIcon(
|
|
1399
|
-
'question',
|
|
1400
|
-
'#dc2626',
|
|
1401
|
-
12
|
|
1402
|
-
)}Missing data?</strong>
|
|
1403
|
-
<span style="color: #991b1b;"> Check relationships and join conditions</span>
|
|
1404
|
-
</div>
|
|
1405
|
-
<div style="margin-bottom: 8px;">
|
|
1406
|
-
<strong style="color: #dc2626;">${getIcon(
|
|
1407
|
-
'warning',
|
|
1408
|
-
'#dc2626',
|
|
1409
|
-
12
|
|
1410
|
-
)}Export failing?</strong>
|
|
1411
|
-
<span style="color: #991b1b;"> Try smaller date ranges or fewer rows</span>
|
|
1412
|
-
</div>
|
|
1413
|
-
<div>
|
|
1414
|
-
<strong style="color: #dc2626;">${getIcon(
|
|
1415
|
-
'eye',
|
|
1416
|
-
'#dc2626',
|
|
1417
|
-
12
|
|
1418
|
-
)}No data showing?</strong>
|
|
1419
|
-
<span style="color: #991b1b;"> Review your filter conditions</span>
|
|
1420
|
-
</div>
|
|
1421
|
-
</div>
|
|
1422
|
-
</div>
|
|
1423
|
-
</div>
|
|
1424
|
-
`,
|
|
1801
|
+
)}${titleText}</div>`,
|
|
1802
|
+
html: `<div style="text-align: left; font-size: 13px; line-height: 1.5; color: #374151; max-height: 70vh; overflow-y: auto;">${sections.join(
|
|
1803
|
+
'<div style="height: 12px;"></div>'
|
|
1804
|
+
)}</div>`,
|
|
1425
1805
|
width: 800,
|
|
1426
1806
|
confirmButtonText: 'Close',
|
|
1427
1807
|
confirmButtonColor: '#6b7280',
|
|
@@ -1432,22 +1812,351 @@ const GenericReportImproved = ({
|
|
|
1432
1812
|
htmlContainer: 'help-content-scrollable',
|
|
1433
1813
|
},
|
|
1434
1814
|
});
|
|
1435
|
-
|
|
1815
|
+
|
|
1816
|
+
/**
|
|
1817
|
+
* Help for the semantic wizard: the user picks subjects, related areas and
|
|
1818
|
+
* details by their business names, so the guide never mentions a table, a
|
|
1819
|
+
* column or a join.
|
|
1820
|
+
*/
|
|
1821
|
+
const showSemanticGuidedHelp = () =>
|
|
1822
|
+
showHelpModal('chart', 'Report Builder Guide', [
|
|
1823
|
+
helpHeading('rocket', '#3b82f6', 'Getting started') +
|
|
1824
|
+
helpPanel({
|
|
1825
|
+
bg: '#f0f9ff',
|
|
1826
|
+
border: '#3b82f6',
|
|
1827
|
+
text: '#1e3a8a',
|
|
1828
|
+
icon: 'file',
|
|
1829
|
+
title: 'Open a report someone already built',
|
|
1830
|
+
items: [
|
|
1831
|
+
'The builder opens on "Choose a Starting Point"',
|
|
1832
|
+
'Click any saved report to open it, change it and run it',
|
|
1833
|
+
'A padlock means only you can see it; a globe means everyone can',
|
|
1834
|
+
],
|
|
1835
|
+
}) +
|
|
1836
|
+
helpPanel({
|
|
1837
|
+
bg: '#f9fafb',
|
|
1838
|
+
border: '#6b7280',
|
|
1839
|
+
text: '#374151',
|
|
1840
|
+
icon: 'cogs',
|
|
1841
|
+
title: 'Build a new one',
|
|
1842
|
+
items: [
|
|
1843
|
+
'Click "Create New Report" and work through the six steps',
|
|
1844
|
+
'Only two things are compulsory: what the report is about, and at least one detail to show',
|
|
1845
|
+
'Related information, filters and grouping can all be skipped',
|
|
1846
|
+
],
|
|
1847
|
+
}),
|
|
1848
|
+
|
|
1849
|
+
helpHeading('list', '#10b981', 'The six steps') +
|
|
1850
|
+
helpPanel({
|
|
1851
|
+
bg: '#ecfdf5',
|
|
1852
|
+
border: '#10b981',
|
|
1853
|
+
text: '#065f46',
|
|
1854
|
+
icon: 'database',
|
|
1855
|
+
title: '1. Choose Your Subject',
|
|
1856
|
+
items: [
|
|
1857
|
+
'Each card is a kind of record your practice keeps, in plain English',
|
|
1858
|
+
'The card tells you how many details and related areas it has',
|
|
1859
|
+
'Pick the one you want one row per — for example one row per client',
|
|
1860
|
+
],
|
|
1861
|
+
}) +
|
|
1862
|
+
helpPanel({
|
|
1863
|
+
bg: '#eff6ff',
|
|
1864
|
+
border: '#3b82f6',
|
|
1865
|
+
text: '#1e3a8a',
|
|
1866
|
+
icon: 'link',
|
|
1867
|
+
title: '2. Add Related Data (optional)',
|
|
1868
|
+
items: [
|
|
1869
|
+
'Each card is an area already connected to your subject — nothing to set up',
|
|
1870
|
+
'Click one to make its details available at the next step',
|
|
1871
|
+
'Once added, areas connected to that one appear too, up to three steps out',
|
|
1872
|
+
'An area marked "many per record" can repeat a record across several rows',
|
|
1873
|
+
],
|
|
1874
|
+
}) +
|
|
1875
|
+
helpPanel({
|
|
1876
|
+
bg: '#f0fdf4',
|
|
1877
|
+
border: '#22c55e',
|
|
1878
|
+
text: '#166534',
|
|
1879
|
+
icon: 'columns',
|
|
1880
|
+
title: '3. Choose Information',
|
|
1881
|
+
items: [
|
|
1882
|
+
'Details are listed under the area they come from, then sorted into Details, Amounts & numbers, Dates, and Status & yes/no',
|
|
1883
|
+
'Tick anything you want as a column',
|
|
1884
|
+
'Each ticked detail gets a "Show as" picker — see Totals and counts below',
|
|
1885
|
+
'"Columns in your report" lists your choices left to right; use Up, Down and the X to reorder or remove them',
|
|
1886
|
+
],
|
|
1887
|
+
}) +
|
|
1888
|
+
helpPanel({
|
|
1889
|
+
bg: '#fdf2f8',
|
|
1890
|
+
border: '#ec4899',
|
|
1891
|
+
text: '#9d174d',
|
|
1892
|
+
icon: 'filter',
|
|
1893
|
+
title: '4. Filter Results (optional)',
|
|
1894
|
+
items: [
|
|
1895
|
+
'Add a condition, choose a detail, then a plain-English test: is, is not, contains, is between, is more than, is blank, is any of, and so on',
|
|
1896
|
+
'The value box matches the detail — a date picker for dates, a list of choices for a status',
|
|
1897
|
+
'Switch a group between "all of these" and "any of these"',
|
|
1898
|
+
'Add a nested group to mix the two, e.g. active AND (has email OR has phone)',
|
|
1899
|
+
'No conditions means every record is included',
|
|
1900
|
+
],
|
|
1901
|
+
}) +
|
|
1902
|
+
helpPanel({
|
|
1903
|
+
bg: '#fef3c7',
|
|
1904
|
+
border: '#f59e0b',
|
|
1905
|
+
text: '#92400e',
|
|
1906
|
+
icon: 'database',
|
|
1907
|
+
title: '5. Group & Order (optional)',
|
|
1908
|
+
items: [
|
|
1909
|
+
'Tick a column to break the results into sections by its value',
|
|
1910
|
+
'Only plain columns can be grouped — a summarised column is the answer, not the heading',
|
|
1911
|
+
'Add one or more sorts to control the row order, each A to Z / low to high or the reverse',
|
|
1912
|
+
],
|
|
1913
|
+
}) +
|
|
1914
|
+
helpPanel({
|
|
1915
|
+
bg: '#f3e8ff',
|
|
1916
|
+
border: '#8b5cf6',
|
|
1917
|
+
text: '#6b21a8',
|
|
1918
|
+
icon: 'eye',
|
|
1919
|
+
title: '6. Preview & Save',
|
|
1920
|
+
items: [
|
|
1921
|
+
'Answer any run-time questions, then click "Run report"',
|
|
1922
|
+
'Results appear 10, 25, 50 or 100 rows at a time',
|
|
1923
|
+
'Save the report, and export the results when they look right',
|
|
1924
|
+
],
|
|
1925
|
+
}),
|
|
1926
|
+
|
|
1927
|
+
helpHeading('money', '#0891b2', 'Totals, counts and averages') +
|
|
1928
|
+
helpPanel({
|
|
1929
|
+
bg: '#ecfeff',
|
|
1930
|
+
border: '#0891b2',
|
|
1931
|
+
text: '#155e75',
|
|
1932
|
+
items: [
|
|
1933
|
+
'"Show as" is set to <strong>Value</strong> by default, which simply lists the detail',
|
|
1934
|
+
'Change it to <strong>Total</strong>, <strong>Average</strong>, <strong>Lowest</strong> or <strong>Highest</strong> on amounts and numbers, <strong>Earliest</strong> or <strong>Latest</strong> on dates, and <strong>Count</strong> on anything',
|
|
1935
|
+
'As soon as one column is summarised, the report collapses to one row per combination of the plain columns you kept',
|
|
1936
|
+
'So for totals by adviser: choose Clients, show the adviser’s name as Value and the fee amount as Total — you get one row per adviser',
|
|
1937
|
+
'If you also tick grouping at step 5 while a column is summarised, tick <em>every</em> plain column, or the report will be refused',
|
|
1938
|
+
],
|
|
1939
|
+
}),
|
|
1940
|
+
|
|
1941
|
+
helpHeading('question', '#7c3aed', 'Reports that ask a question each time') +
|
|
1942
|
+
helpPanel({
|
|
1943
|
+
bg: '#f5f3ff',
|
|
1944
|
+
border: '#7c3aed',
|
|
1945
|
+
text: '#5b21b6',
|
|
1946
|
+
items: [
|
|
1947
|
+
'On any condition, tick "Ask me for this each time I run it"',
|
|
1948
|
+
'The value is left out of the saved report and asked for instead, under "Before we run this report"',
|
|
1949
|
+
'Give each question a clear name — that wording is what everyone else will see',
|
|
1950
|
+
'Untick "Must be answered" to make a question optional',
|
|
1951
|
+
'Ideal for anything that changes week to week, such as a date range',
|
|
1952
|
+
],
|
|
1953
|
+
}),
|
|
1954
|
+
|
|
1955
|
+
helpHeading('users', '#059669', 'Saving and sharing') +
|
|
1956
|
+
helpPanel({
|
|
1957
|
+
bg: '#ecfdf5',
|
|
1958
|
+
border: '#059669',
|
|
1959
|
+
text: '#065f46',
|
|
1960
|
+
items: [
|
|
1961
|
+
'"Save report" asks for a name and whether to make it public',
|
|
1962
|
+
'Private reports (padlock) are yours; public ones (globe) are visible to other users',
|
|
1963
|
+
'Opening a saved report and saving again updates it — use the copy option to keep the original',
|
|
1964
|
+
'A saved report stores the recipe, not the results, so it always runs against today’s data',
|
|
1965
|
+
],
|
|
1966
|
+
}),
|
|
1967
|
+
|
|
1968
|
+
helpHeading('download', '#b45309', 'Exporting') +
|
|
1969
|
+
helpPanel({
|
|
1970
|
+
bg: '#fffbeb',
|
|
1971
|
+
border: '#b45309',
|
|
1972
|
+
text: '#92400e',
|
|
1973
|
+
items: [
|
|
1974
|
+
'Run the report first, then choose Excel, CSV or PDF from "Export as"',
|
|
1975
|
+
'The export covers the whole report, not just the page on screen',
|
|
1976
|
+
'PDF suits short reports; use Excel or CSV for long ones',
|
|
1977
|
+
],
|
|
1978
|
+
}),
|
|
1979
|
+
|
|
1980
|
+
helpHeading('warning', '#dc2626', 'Good to know') +
|
|
1981
|
+
helpPanel({
|
|
1982
|
+
bg: '#fef2f2',
|
|
1983
|
+
border: '#dc2626',
|
|
1984
|
+
text: '#991b1b',
|
|
1985
|
+
items: [
|
|
1986
|
+
'Deleted records are left out of reports entirely — they are missing from the rows, and from every count and total',
|
|
1987
|
+
'Adding a "many per record" area can multiply your rows; summarise it instead to keep one row per record',
|
|
1988
|
+
'Nothing shown at all? Check your conditions at step 4 — one condition too many is the usual cause',
|
|
1989
|
+
'If the report is refused, the message on screen names the detail that caused it',
|
|
1990
|
+
],
|
|
1991
|
+
}),
|
|
1992
|
+
]);
|
|
1993
|
+
|
|
1994
|
+
/**
|
|
1995
|
+
* Help for the legacy table/column wizard, used when the newer report
|
|
1996
|
+
* engine is not available.
|
|
1997
|
+
*/
|
|
1998
|
+
const showLegacyGuidedHelp = () =>
|
|
1999
|
+
showHelpModal('chart', 'Complete Report Builder Guide', [
|
|
2000
|
+
helpHeading('rocket', '#3b82f6', 'Getting Started') +
|
|
2001
|
+
helpPanel({
|
|
2002
|
+
bg: '#f0f9ff',
|
|
2003
|
+
border: '#3b82f6',
|
|
2004
|
+
text: '#1e3a8a',
|
|
2005
|
+
icon: 'file',
|
|
2006
|
+
title: 'Option 1: Start from a saved report (recommended)',
|
|
2007
|
+
items: [
|
|
2008
|
+
'The builder opens on "Choose a Starting Point"',
|
|
2009
|
+
'Click any saved report to open it and adjust it',
|
|
2010
|
+
'A padlock means only you can see it; a globe means everyone can',
|
|
2011
|
+
],
|
|
2012
|
+
}) +
|
|
2013
|
+
helpPanel({
|
|
2014
|
+
bg: '#f9fafb',
|
|
2015
|
+
border: '#6b7280',
|
|
2016
|
+
text: '#374151',
|
|
2017
|
+
icon: 'cogs',
|
|
2018
|
+
title: 'Option 2: Build a custom report',
|
|
2019
|
+
items: [
|
|
2020
|
+
'Click "Create New Report" and follow the six steps below',
|
|
2021
|
+
],
|
|
2022
|
+
}),
|
|
2023
|
+
|
|
2024
|
+
helpHeading('list', '#10b981', 'Six steps to a report') +
|
|
2025
|
+
helpPanel({
|
|
2026
|
+
bg: '#ecfdf5',
|
|
2027
|
+
border: '#10b981',
|
|
2028
|
+
text: '#065f46',
|
|
2029
|
+
icon: 'database',
|
|
2030
|
+
title: 'Step 1: Select Your Data',
|
|
2031
|
+
items: [
|
|
2032
|
+
'Choose the main data source your report is about',
|
|
2033
|
+
'This becomes the foundation of the report',
|
|
2034
|
+
'You can bring in related data at the next step',
|
|
2035
|
+
],
|
|
2036
|
+
}) +
|
|
2037
|
+
helpPanel({
|
|
2038
|
+
bg: '#fef3c7',
|
|
2039
|
+
border: '#f59e0b',
|
|
2040
|
+
text: '#92400e',
|
|
2041
|
+
icon: 'link',
|
|
2042
|
+
title: 'Step 2: Add Related Data (optional)',
|
|
2043
|
+
items: [
|
|
2044
|
+
'Review the connections suggested for you',
|
|
2045
|
+
'Connect the ones that add value, e.g. customer names on invoices',
|
|
2046
|
+
'Connected data provides extra fields at the next step',
|
|
2047
|
+
'Skip this step if the main data source has everything you need',
|
|
2048
|
+
],
|
|
2049
|
+
}) +
|
|
2050
|
+
helpPanel({
|
|
2051
|
+
bg: '#eff6ff',
|
|
2052
|
+
border: '#3b82f6',
|
|
2053
|
+
text: '#1e3a8a',
|
|
2054
|
+
icon: 'columns',
|
|
2055
|
+
title: 'Step 3: Choose Information',
|
|
2056
|
+
items: [
|
|
2057
|
+
'Tick the fields you want to see, from your main data source and anything you connected',
|
|
2058
|
+
'Fields are grouped by category to make them easier to find',
|
|
2059
|
+
'Use "Select All Visible" or "Clear All" to work quickly',
|
|
2060
|
+
],
|
|
2061
|
+
}) +
|
|
2062
|
+
helpPanel({
|
|
2063
|
+
bg: '#fdf2f8',
|
|
2064
|
+
border: '#ec4899',
|
|
2065
|
+
text: '#9d174d',
|
|
2066
|
+
icon: 'filter',
|
|
2067
|
+
title: 'Step 4: Filter Results (optional)',
|
|
2068
|
+
items: [
|
|
2069
|
+
'Narrow the results down, e.g. active clients only',
|
|
2070
|
+
'Combine conditions with AND/OR logic',
|
|
2071
|
+
'Group conditions together for more involved rules',
|
|
2072
|
+
'Great for date ranges and status filters',
|
|
2073
|
+
],
|
|
2074
|
+
}) +
|
|
2075
|
+
helpPanel({
|
|
2076
|
+
bg: '#f0fdf4',
|
|
2077
|
+
border: '#22c55e',
|
|
2078
|
+
text: '#166534',
|
|
2079
|
+
icon: 'database',
|
|
2080
|
+
title: 'Step 5: Group Configuration (optional)',
|
|
2081
|
+
items: [
|
|
2082
|
+
'Turn on grouping to split the report into sections',
|
|
2083
|
+
'Choose the field to group by, e.g. status or category',
|
|
2084
|
+
'Set the group display and styling options',
|
|
2085
|
+
'Skip this step for a plain table',
|
|
2086
|
+
],
|
|
2087
|
+
}) +
|
|
2088
|
+
helpPanel({
|
|
2089
|
+
bg: '#f3e8ff',
|
|
2090
|
+
border: '#8b5cf6',
|
|
2091
|
+
text: '#6b21a8',
|
|
2092
|
+
icon: 'download',
|
|
2093
|
+
title: 'Step 6: Preview & Save',
|
|
2094
|
+
items: [
|
|
2095
|
+
'Click "Generate Report" to see your data',
|
|
2096
|
+
'Review the results and refine as needed',
|
|
2097
|
+
'Save the report so you can run it again later',
|
|
2098
|
+
'Click "Export" to download the results as an Excel file',
|
|
2099
|
+
],
|
|
2100
|
+
}),
|
|
2101
|
+
|
|
2102
|
+
helpHeading('star', '#f59e0b', 'Pro Tips') +
|
|
2103
|
+
helpPanel({
|
|
2104
|
+
bg: '#fffbeb',
|
|
2105
|
+
border: '#f59e0b',
|
|
2106
|
+
text: '#92400e',
|
|
2107
|
+
items: [
|
|
2108
|
+
'<strong>Start simple:</strong> begin with a basic report, then add to it',
|
|
2109
|
+
'<strong>Save before exporting:</strong> an export needs the report to have a name',
|
|
2110
|
+
'<strong>Save your work:</strong> a saved report can be run again any time against current data',
|
|
2111
|
+
'<strong>Date filters:</strong> narrowing the dates keeps big reports fast',
|
|
2112
|
+
'<strong>Field limits:</strong> too many fields can slow a report down',
|
|
2113
|
+
],
|
|
2114
|
+
}),
|
|
2115
|
+
|
|
2116
|
+
helpHeading('settings', '#dc2626', 'Common Issues') +
|
|
2117
|
+
helpPanel({
|
|
2118
|
+
bg: '#fef2f2',
|
|
2119
|
+
border: '#dc2626',
|
|
2120
|
+
text: '#991b1b',
|
|
2121
|
+
items: [
|
|
2122
|
+
'<strong>Report too slow?</strong> Add date filters or reduce the number of fields',
|
|
2123
|
+
'<strong>Missing data?</strong> Check the connections you added at step 2',
|
|
2124
|
+
'<strong>Export failing?</strong> Try a smaller date range or fewer rows',
|
|
2125
|
+
'<strong>No data showing?</strong> Review your filter conditions',
|
|
2126
|
+
],
|
|
2127
|
+
}),
|
|
2128
|
+
]);
|
|
2129
|
+
|
|
2130
|
+
/** The guide matches whichever wizard the user is actually looking at. */
|
|
2131
|
+
const showGuidedHelp = () =>
|
|
2132
|
+
isSemantic ? showSemanticGuidedHelp() : showLegacyGuidedHelp();
|
|
1436
2133
|
|
|
1437
2134
|
// Initialize component
|
|
1438
2135
|
useEffect(() => {
|
|
1439
|
-
|
|
2136
|
+
// Wait for the semantic probe to settle before hitting the legacy
|
|
2137
|
+
// table endpoint: a semantic-only backend has no /getTables to call,
|
|
2138
|
+
// and asking anyway would surface an error the user cannot act on.
|
|
2139
|
+
if (semanticStatus === 'loading') return;
|
|
2140
|
+
if (semanticStatus !== 'ready') {
|
|
2141
|
+
fetchDatabaseTables();
|
|
2142
|
+
}
|
|
2143
|
+
|
|
1440
2144
|
fetchSavedReports();
|
|
1441
2145
|
|
|
1442
|
-
// Show help on first load
|
|
1443
|
-
|
|
2146
|
+
// Show help on first load. The two wizards keep separate flags: users
|
|
2147
|
+
// who already dismissed the legacy guide have never seen the semantic
|
|
2148
|
+
// one, and it describes a different set of steps.
|
|
2149
|
+
const helpSeenKey = isSemantic
|
|
2150
|
+
? 'reportBuilder_hasSeenSemanticHelp'
|
|
2151
|
+
: 'reportBuilder_hasSeenHelp';
|
|
2152
|
+
const hasSeenHelp = localStorage.getItem(helpSeenKey);
|
|
1444
2153
|
if (!hasSeenHelp) {
|
|
1445
2154
|
setTimeout(() => {
|
|
1446
2155
|
showGuidedHelp();
|
|
1447
|
-
localStorage.setItem(
|
|
2156
|
+
localStorage.setItem(helpSeenKey, 'true');
|
|
1448
2157
|
}, 500);
|
|
1449
2158
|
}
|
|
1450
|
-
}, [tableUrl, reportsUrl]); //
|
|
2159
|
+
}, [tableUrl, reportsUrl, semanticStatus]); // Re-fetch when URLs change
|
|
1451
2160
|
|
|
1452
2161
|
// Fetch columns when table is selected (like advanced mode)
|
|
1453
2162
|
useEffect(() => {
|
|
@@ -1522,6 +2231,7 @@ const GenericReportImproved = ({
|
|
|
1522
2231
|
// Auto-execute report when user reaches step 5 (preview step)
|
|
1523
2232
|
useEffect(() => {
|
|
1524
2233
|
if (
|
|
2234
|
+
!isSemantic && // Semantic mode has its own run trigger below
|
|
1525
2235
|
currentWizardStep === 5 && // Step 6 is index 5 (preview step)
|
|
1526
2236
|
selectedTable &&
|
|
1527
2237
|
selectedColumns.length > 0 &&
|
|
@@ -1532,6 +2242,7 @@ const GenericReportImproved = ({
|
|
|
1532
2242
|
executeReport();
|
|
1533
2243
|
}
|
|
1534
2244
|
}, [
|
|
2245
|
+
isSemantic,
|
|
1535
2246
|
currentWizardStep,
|
|
1536
2247
|
selectedTable,
|
|
1537
2248
|
selectedColumns.length,
|
|
@@ -1539,6 +2250,40 @@ const GenericReportImproved = ({
|
|
|
1539
2250
|
hasAutoExecuted,
|
|
1540
2251
|
]);
|
|
1541
2252
|
|
|
2253
|
+
// Semantic mode: run once on arriving at the preview step, but only when
|
|
2254
|
+
// there is nothing to ask the user first — otherwise wait for their answers.
|
|
2255
|
+
useEffect(() => {
|
|
2256
|
+
if (!isSemantic) return;
|
|
2257
|
+
if (currentWizardStep !== 5) return;
|
|
2258
|
+
if (isLoading || semanticHasRun) return;
|
|
2259
|
+
if (semanticSelections.length === 0) return;
|
|
2260
|
+
if (!parametersSatisfied(semanticParameters, semanticParameterValues)) {
|
|
2261
|
+
return;
|
|
2262
|
+
}
|
|
2263
|
+
runSemanticReport({ page: 1 });
|
|
2264
|
+
}, [
|
|
2265
|
+
isSemantic,
|
|
2266
|
+
currentWizardStep,
|
|
2267
|
+
isLoading,
|
|
2268
|
+
semanticHasRun,
|
|
2269
|
+
semanticSelections.length,
|
|
2270
|
+
semanticParameters,
|
|
2271
|
+
semanticParameterValues,
|
|
2272
|
+
]);
|
|
2273
|
+
|
|
2274
|
+
// Any change to the report design invalidates the results already on screen.
|
|
2275
|
+
useEffect(() => {
|
|
2276
|
+
if (!isSemantic) return;
|
|
2277
|
+
setSemanticHasRun(false);
|
|
2278
|
+
}, [
|
|
2279
|
+
isSemantic,
|
|
2280
|
+
semanticEntity,
|
|
2281
|
+
semanticSelections,
|
|
2282
|
+
semanticFilterTree,
|
|
2283
|
+
semanticGroupBy,
|
|
2284
|
+
semanticSort,
|
|
2285
|
+
]);
|
|
2286
|
+
|
|
1542
2287
|
// Escape closes whichever modal is open
|
|
1543
2288
|
useEffect(() => {
|
|
1544
2289
|
if (!showCalculatedFieldModal && !showRowExclusionModal) return undefined;
|
|
@@ -1724,6 +2469,9 @@ const GenericReportImproved = ({
|
|
|
1724
2469
|
// Reset UI state
|
|
1725
2470
|
setShowHiddenFields(false);
|
|
1726
2471
|
|
|
2472
|
+
// Reset semantic mode state (inert in legacy mode)
|
|
2473
|
+
resetSemanticState();
|
|
2474
|
+
|
|
1727
2475
|
toast.info(
|
|
1728
2476
|
'Report builder reset. You can start creating a new report.'
|
|
1729
2477
|
);
|
|
@@ -1731,7 +2479,7 @@ const GenericReportImproved = ({
|
|
|
1731
2479
|
|
|
1732
2480
|
// Wizard navigation
|
|
1733
2481
|
const goToNextStep = () => {
|
|
1734
|
-
if (currentWizardStep <
|
|
2482
|
+
if (currentWizardStep < activeWizardSteps.length - 1) {
|
|
1735
2483
|
setCurrentWizardStep(currentWizardStep + 1);
|
|
1736
2484
|
}
|
|
1737
2485
|
};
|
|
@@ -1744,6 +2492,25 @@ const GenericReportImproved = ({
|
|
|
1744
2492
|
|
|
1745
2493
|
// Check if current step is complete
|
|
1746
2494
|
const isStepComplete = (stepId) => {
|
|
2495
|
+
if (isSemantic) {
|
|
2496
|
+
switch (stepId) {
|
|
2497
|
+
case 'table':
|
|
2498
|
+
return !!semanticEntity;
|
|
2499
|
+
case 'relationships':
|
|
2500
|
+
return true; // Optional step
|
|
2501
|
+
case 'columns':
|
|
2502
|
+
return semanticSelections.length > 0;
|
|
2503
|
+
case 'filters':
|
|
2504
|
+
return true; // Optional step
|
|
2505
|
+
case 'grouping':
|
|
2506
|
+
return true; // Optional step
|
|
2507
|
+
case 'preview':
|
|
2508
|
+
return semanticHasRun;
|
|
2509
|
+
default:
|
|
2510
|
+
return false;
|
|
2511
|
+
}
|
|
2512
|
+
}
|
|
2513
|
+
|
|
1747
2514
|
switch (stepId) {
|
|
1748
2515
|
case 'table':
|
|
1749
2516
|
return !!selectedTable;
|
|
@@ -1813,15 +2580,15 @@ const GenericReportImproved = ({
|
|
|
1813
2580
|
return;
|
|
1814
2581
|
}
|
|
1815
2582
|
|
|
1816
|
-
// Filter out hidden tables
|
|
2583
|
+
// Filter out hidden tables only. Snake_case names are ordinary
|
|
2584
|
+
// table names — dropping them silently hid most of the schema.
|
|
1817
2585
|
let filteredTables = tableData
|
|
1818
2586
|
.filter((table) => {
|
|
1819
2587
|
const tableName =
|
|
1820
2588
|
typeof table === 'string' ? table : table?.name;
|
|
1821
2589
|
return (
|
|
1822
2590
|
typeof tableName === 'string' &&
|
|
1823
|
-
!hiddenTables.includes(tableName)
|
|
1824
|
-
!tableName.includes('_')
|
|
2591
|
+
!hiddenTables.includes(tableName)
|
|
1825
2592
|
);
|
|
1826
2593
|
})
|
|
1827
2594
|
.map((table) => {
|
|
@@ -2988,8 +3755,8 @@ const GenericReportImproved = ({
|
|
|
2988
3755
|
}
|
|
2989
3756
|
}
|
|
2990
3757
|
|
|
2991
|
-
if (currentWizardStep <
|
|
2992
|
-
setCurrentWizardStep(
|
|
3758
|
+
if (currentWizardStep < activeWizardSteps.length - 1) {
|
|
3759
|
+
setCurrentWizardStep(activeWizardSteps.length - 1);
|
|
2993
3760
|
}
|
|
2994
3761
|
|
|
2995
3762
|
// Report executed successfully
|
|
@@ -3033,6 +3800,40 @@ const GenericReportImproved = ({
|
|
|
3033
3800
|
return;
|
|
3034
3801
|
}
|
|
3035
3802
|
|
|
3803
|
+
// A v2 document restores the semantic wizard. A v1 document (it has
|
|
3804
|
+
// `mainTable`) always falls through to the legacy path below, even
|
|
3805
|
+
// when the semantic model happens to be available.
|
|
3806
|
+
if (isSemanticDefinition(config)) {
|
|
3807
|
+
if (!isSemantic) {
|
|
3808
|
+
toast.error(
|
|
3809
|
+
'This report needs the newer report engine, which is not available here.'
|
|
3810
|
+
);
|
|
3811
|
+
return;
|
|
3812
|
+
}
|
|
3813
|
+
|
|
3814
|
+
const parsed = parseDefinition(config);
|
|
3815
|
+
resetSemanticState();
|
|
3816
|
+
setSemanticEntity(parsed.entity);
|
|
3817
|
+
setSemanticSelections(parsed.selections);
|
|
3818
|
+
setSemanticFilterTree(parsed.filterTree);
|
|
3819
|
+
setSemanticParameters(parsed.parameters);
|
|
3820
|
+
setSemanticGroupBy(parsed.groupBy);
|
|
3821
|
+
setSemanticSort(parsed.sort);
|
|
3822
|
+
// Re-derive which related areas the saved paths reach through,
|
|
3823
|
+
// so the relations step shows them as already added.
|
|
3824
|
+
setSemanticRelations(
|
|
3825
|
+
relationPathsFromDefinition(parsed, semanticModel)
|
|
3826
|
+
);
|
|
3827
|
+
|
|
3828
|
+
setReportName(report.label || '');
|
|
3829
|
+
setIsPublic(!!report.is_public);
|
|
3830
|
+
setLoadedReportId(report.id);
|
|
3831
|
+
setShowTemplates(false);
|
|
3832
|
+
setCurrentWizardStep(0);
|
|
3833
|
+
toast.success(`Loaded report: ${report.label}`);
|
|
3834
|
+
return;
|
|
3835
|
+
}
|
|
3836
|
+
|
|
3036
3837
|
if (config.mainTable) {
|
|
3037
3838
|
setSelectedTable(config.mainTable);
|
|
3038
3839
|
setAvailableTables([config.mainTable]);
|
|
@@ -3440,9 +4241,21 @@ const GenericReportImproved = ({
|
|
|
3440
4241
|
<h3>Your Saved Reports ({savedReports.length})</h3>
|
|
3441
4242
|
<div className={styles.reportsGrid}>
|
|
3442
4243
|
{savedReports.map((report) => {
|
|
3443
|
-
const
|
|
3444
|
-
|
|
3445
|
-
|
|
4244
|
+
const reportDetail = parseReportDetail(
|
|
4245
|
+
report.detail
|
|
4246
|
+
);
|
|
4247
|
+
// v1 reports show the table name; v2 ones
|
|
4248
|
+
// show the entity's business label.
|
|
4249
|
+
const reportSubject = isSemanticDefinition(
|
|
4250
|
+
reportDetail
|
|
4251
|
+
)
|
|
4252
|
+
? getEntityLabel(
|
|
4253
|
+
semanticModel,
|
|
4254
|
+
reportDetail.entity
|
|
4255
|
+
)
|
|
4256
|
+
: formatName(
|
|
4257
|
+
reportDetail?.mainTable || ''
|
|
4258
|
+
);
|
|
3446
4259
|
return (
|
|
3447
4260
|
<div
|
|
3448
4261
|
key={report.id}
|
|
@@ -3497,7 +4310,7 @@ const GenericReportImproved = ({
|
|
|
3497
4310
|
</span>
|
|
3498
4311
|
</div>
|
|
3499
4312
|
</div>
|
|
3500
|
-
{
|
|
4313
|
+
{reportSubject && (
|
|
3501
4314
|
<div
|
|
3502
4315
|
className={
|
|
3503
4316
|
styles.reportTableInfo
|
|
@@ -3517,9 +4330,7 @@ const GenericReportImproved = ({
|
|
|
3517
4330
|
'middle',
|
|
3518
4331
|
}}
|
|
3519
4332
|
/>
|
|
3520
|
-
{
|
|
3521
|
-
reportMainTable
|
|
3522
|
-
)}
|
|
4333
|
+
{reportSubject}
|
|
3523
4334
|
</span>
|
|
3524
4335
|
</div>
|
|
3525
4336
|
)}
|
|
@@ -3571,14 +4382,14 @@ const GenericReportImproved = ({
|
|
|
3571
4382
|
|
|
3572
4383
|
// Render wizard mode
|
|
3573
4384
|
const renderWizardMode = () => {
|
|
3574
|
-
const currentStep =
|
|
4385
|
+
const currentStep = activeWizardSteps[currentWizardStep];
|
|
3575
4386
|
const StepIcon = currentStep.icon;
|
|
3576
4387
|
|
|
3577
4388
|
return (
|
|
3578
4389
|
<div className={styles.wizardMode}>
|
|
3579
4390
|
{/* Progress bar */}
|
|
3580
4391
|
<div className={styles.wizardProgress}>
|
|
3581
|
-
{
|
|
4392
|
+
{activeWizardSteps.map((step, index) => {
|
|
3582
4393
|
const StepIcon = step.icon;
|
|
3583
4394
|
const isActive = index === currentWizardStep;
|
|
3584
4395
|
const isComplete =
|
|
@@ -3696,7 +4507,7 @@ const GenericReportImproved = ({
|
|
|
3696
4507
|
</button>
|
|
3697
4508
|
)}
|
|
3698
4509
|
|
|
3699
|
-
{currentWizardStep ===
|
|
4510
|
+
{currentWizardStep === activeWizardSteps.length - 1 ? (
|
|
3700
4511
|
<button
|
|
3701
4512
|
type="button"
|
|
3702
4513
|
className={`${styles.btn} ${styles.btnStartAgain}`}
|
|
@@ -3720,8 +4531,107 @@ const GenericReportImproved = ({
|
|
|
3720
4531
|
);
|
|
3721
4532
|
};
|
|
3722
4533
|
|
|
4534
|
+
// Render the semantic-mode body for a step. Returns null in legacy mode so
|
|
4535
|
+
// the caller falls through to the original renderers untouched.
|
|
4536
|
+
const renderSemanticStepContent = (stepId) => {
|
|
4537
|
+
switch (stepId) {
|
|
4538
|
+
case 'table':
|
|
4539
|
+
return (
|
|
4540
|
+
<SemanticEntityStep
|
|
4541
|
+
model={semanticModel}
|
|
4542
|
+
selectedEntity={semanticEntity}
|
|
4543
|
+
onSelect={handleSemanticSelectEntity}
|
|
4544
|
+
/>
|
|
4545
|
+
);
|
|
4546
|
+
case 'relationships':
|
|
4547
|
+
return (
|
|
4548
|
+
<SemanticRelationsStep
|
|
4549
|
+
model={semanticModel}
|
|
4550
|
+
entityId={semanticEntity}
|
|
4551
|
+
addedRelations={semanticRelations}
|
|
4552
|
+
onToggle={handleSemanticToggleRelation}
|
|
4553
|
+
/>
|
|
4554
|
+
);
|
|
4555
|
+
case 'columns':
|
|
4556
|
+
return (
|
|
4557
|
+
<SemanticFieldsStep
|
|
4558
|
+
model={semanticModel}
|
|
4559
|
+
entityId={semanticEntity}
|
|
4560
|
+
addedRelations={semanticRelations}
|
|
4561
|
+
selections={semanticSelections}
|
|
4562
|
+
onToggleField={handleSemanticToggleField}
|
|
4563
|
+
onChangeAggregate={handleSemanticChangeAggregate}
|
|
4564
|
+
onRemoveSelection={handleSemanticRemoveSelection}
|
|
4565
|
+
onMoveSelection={handleSemanticMoveSelection}
|
|
4566
|
+
/>
|
|
4567
|
+
);
|
|
4568
|
+
case 'filters':
|
|
4569
|
+
return (
|
|
4570
|
+
<SemanticFiltersStep
|
|
4571
|
+
model={semanticModel}
|
|
4572
|
+
entityId={semanticEntity}
|
|
4573
|
+
addedRelations={semanticRelations}
|
|
4574
|
+
filterTree={semanticFilterTree}
|
|
4575
|
+
parameters={semanticParameters}
|
|
4576
|
+
onChangeNode={handleSemanticChangeFilterNode}
|
|
4577
|
+
onRemoveNode={handleSemanticRemoveFilterNode}
|
|
4578
|
+
onAddCondition={handleSemanticAddFilterCondition}
|
|
4579
|
+
onAddGroup={handleSemanticAddFilterGroup}
|
|
4580
|
+
onChangeParameter={handleSemanticChangeParameter}
|
|
4581
|
+
/>
|
|
4582
|
+
);
|
|
4583
|
+
case 'grouping':
|
|
4584
|
+
return (
|
|
4585
|
+
<SemanticGroupingStep
|
|
4586
|
+
model={semanticModel}
|
|
4587
|
+
entityId={semanticEntity}
|
|
4588
|
+
selections={semanticSelections}
|
|
4589
|
+
groupBy={semanticGroupBy}
|
|
4590
|
+
sort={semanticSort}
|
|
4591
|
+
onToggleGroupBy={handleSemanticToggleGroupBy}
|
|
4592
|
+
onAddSort={handleSemanticAddSort}
|
|
4593
|
+
onChangeSort={handleSemanticChangeSort}
|
|
4594
|
+
onRemoveSort={handleSemanticRemoveSort}
|
|
4595
|
+
/>
|
|
4596
|
+
);
|
|
4597
|
+
case 'preview':
|
|
4598
|
+
return (
|
|
4599
|
+
<SemanticPreviewStep
|
|
4600
|
+
model={semanticModel}
|
|
4601
|
+
entityId={semanticEntity}
|
|
4602
|
+
selections={semanticSelections}
|
|
4603
|
+
filterTree={semanticFilterTree}
|
|
4604
|
+
parameters={semanticParameters}
|
|
4605
|
+
parameterValues={semanticParameterValues}
|
|
4606
|
+
groupBy={semanticGroupBy}
|
|
4607
|
+
rows={semanticRows}
|
|
4608
|
+
total={semanticTotal}
|
|
4609
|
+
page={semanticPage}
|
|
4610
|
+
pageSize={semanticPageSize}
|
|
4611
|
+
isLoading={isLoading}
|
|
4612
|
+
error={semanticError}
|
|
4613
|
+
onChangeParameterValue={handleSemanticChangeParameterValue}
|
|
4614
|
+
onRun={runSemanticReport}
|
|
4615
|
+
onChangePage={(page) => runSemanticReport({ page })}
|
|
4616
|
+
onChangePageSize={(size) =>
|
|
4617
|
+
runSemanticReport({ page: 1, size })
|
|
4618
|
+
}
|
|
4619
|
+
onExport={exportSemanticReport}
|
|
4620
|
+
onSave={() => saveReport(false)}
|
|
4621
|
+
canSave={semanticSelections.length > 0}
|
|
4622
|
+
/>
|
|
4623
|
+
);
|
|
4624
|
+
default:
|
|
4625
|
+
return null;
|
|
4626
|
+
}
|
|
4627
|
+
};
|
|
4628
|
+
|
|
3723
4629
|
// Render wizard step content
|
|
3724
4630
|
const renderWizardStepContent = (stepId) => {
|
|
4631
|
+
if (isSemantic) {
|
|
4632
|
+
return renderSemanticStepContent(stepId);
|
|
4633
|
+
}
|
|
4634
|
+
|
|
3725
4635
|
switch (stepId) {
|
|
3726
4636
|
case 'table':
|
|
3727
4637
|
return renderTableSelection();
|
|
@@ -7341,10 +8251,15 @@ const GenericReportImproved = ({
|
|
|
7341
8251
|
|
|
7342
8252
|
// Save report configuration (with save/save-as functionality)
|
|
7343
8253
|
const saveReport = async (saveAs = false) => {
|
|
8254
|
+
// In semantic mode the subject is an entity label, not a table name.
|
|
8255
|
+
const subjectName = isSemantic
|
|
8256
|
+
? getEntityLabel(semanticModel, semanticEntity)
|
|
8257
|
+
: formatName(selectedTable);
|
|
8258
|
+
|
|
7344
8259
|
if (!reportName.trim()) {
|
|
7345
|
-
const defaultName = `${
|
|
7346
|
-
|
|
7347
|
-
)}
|
|
8260
|
+
const defaultName = `${subjectName} Report - ${moment().format(
|
|
8261
|
+
'YYYY-MM-DD'
|
|
8262
|
+
)}`;
|
|
7348
8263
|
setReportName(defaultName);
|
|
7349
8264
|
}
|
|
7350
8265
|
|
|
@@ -7377,9 +8292,7 @@ const GenericReportImproved = ({
|
|
|
7377
8292
|
saveAs
|
|
7378
8293
|
? `${reportName} (Copy)`
|
|
7379
8294
|
: reportName ||
|
|
7380
|
-
`${
|
|
7381
|
-
selectedTable
|
|
7382
|
-
)} Report - ${moment().format(
|
|
8295
|
+
`${subjectName} Report - ${moment().format(
|
|
7383
8296
|
'YYYY-MM-DD'
|
|
7384
8297
|
)}`
|
|
7385
8298
|
)}"
|
|
@@ -7416,24 +8329,32 @@ const GenericReportImproved = ({
|
|
|
7416
8329
|
|
|
7417
8330
|
if (result.isConfirmed) {
|
|
7418
8331
|
try {
|
|
7419
|
-
|
|
7420
|
-
|
|
7421
|
-
|
|
7422
|
-
|
|
7423
|
-
|
|
7424
|
-
|
|
7425
|
-
|
|
7426
|
-
|
|
7427
|
-
|
|
7428
|
-
|
|
7429
|
-
|
|
7430
|
-
|
|
7431
|
-
|
|
7432
|
-
|
|
7433
|
-
|
|
7434
|
-
|
|
7435
|
-
|
|
7436
|
-
|
|
8332
|
+
// Semantic reports save the v2 definition document verbatim;
|
|
8333
|
+
// legacy reports keep their existing v1 shape.
|
|
8334
|
+
const reportConfig = isSemantic
|
|
8335
|
+
? {
|
|
8336
|
+
label: result.value.name,
|
|
8337
|
+
is_public: result.value.isPublic,
|
|
8338
|
+
detail: semanticDefinition,
|
|
8339
|
+
}
|
|
8340
|
+
: {
|
|
8341
|
+
label: result.value.name,
|
|
8342
|
+
is_public: result.value.isPublic,
|
|
8343
|
+
detail: {
|
|
8344
|
+
mainTable: selectedTable,
|
|
8345
|
+
columns: selectedColumns,
|
|
8346
|
+
joins: joins,
|
|
8347
|
+
filters: filterCriteria,
|
|
8348
|
+
sorting: sortCriteria,
|
|
8349
|
+
grouping: groupingConfig.enabled
|
|
8350
|
+
? groupingConfig
|
|
8351
|
+
: null,
|
|
8352
|
+
isGrouped: groupingConfig.enabled,
|
|
8353
|
+
customCalculatedFields: customCalculatedFields,
|
|
8354
|
+
customUrls: customUrls,
|
|
8355
|
+
unique: uniqueFieldConfig,
|
|
8356
|
+
},
|
|
8357
|
+
};
|
|
7437
8358
|
|
|
7438
8359
|
let saveResult;
|
|
7439
8360
|
if (isExistingReport) {
|