@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.
@@ -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
- // Show comprehensive help guide
1208
- const showGuidedHelp = () => {
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
- 'chart',
1798
+ titleIcon,
1212
1799
  '#3b82f6',
1213
1800
  24
1214
- )}Complete Report Builder Guide</div>`,
1215
- html: `
1216
- <div style="text-align: left; font-size: 13px; line-height: 1.5; color: #374151; max-height: 70vh; overflow-y: auto;">
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 &amp; numbers, Dates, and Status &amp; 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 &amp; 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 &amp; 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 &amp; 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
- fetchDatabaseTables();
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
- const hasSeenHelp = localStorage.getItem('reportBuilder_hasSeenHelp');
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('reportBuilder_hasSeenHelp', 'true');
2156
+ localStorage.setItem(helpSeenKey, 'true');
1448
2157
  }, 500);
1449
2158
  }
1450
- }, [tableUrl, reportsUrl]); // Add dependencies to ensure re-fetch when URLs change
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 < wizardSteps.length - 1) {
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 and tables with underscores (like advanced mode)
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 < wizardSteps.length - 1) {
2992
- setCurrentWizardStep(wizardSteps.length - 1);
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 reportMainTable =
3444
- parseReportDetail(report.detail)
3445
- ?.mainTable || '';
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
- {reportMainTable && (
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
- {formatName(
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 = wizardSteps[currentWizardStep];
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
- {wizardSteps.map((step, index) => {
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 === wizardSteps.length - 1 ? (
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 = `${formatName(
7346
- selectedTable
7347
- )} Report - ${moment().format('YYYY-MM-DD')}`;
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
- `${formatName(
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
- const reportConfig = {
7420
- label: result.value.name,
7421
- is_public: result.value.isPublic,
7422
- detail: {
7423
- mainTable: selectedTable,
7424
- columns: selectedColumns,
7425
- joins: joins,
7426
- filters: filterCriteria,
7427
- sorting: sortCriteria,
7428
- grouping: groupingConfig.enabled
7429
- ? groupingConfig
7430
- : null,
7431
- isGrouped: groupingConfig.enabled,
7432
- customCalculatedFields: customCalculatedFields,
7433
- customUrls: customUrls,
7434
- unique: uniqueFieldConfig,
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) {