@visns-studio/visns-components 6.2.0 → 6.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -91,7 +91,7 @@
91
91
  "react-dom": "^17.0.0 || ^18.0.0"
92
92
  },
93
93
  "name": "@visns-studio/visns-components",
94
- "version": "6.2.0",
94
+ "version": "6.3.0",
95
95
  "description": "Various packages to assist in the development of our Custom Applications.",
96
96
  "main": "src/index.js",
97
97
  "files": [
@@ -40,7 +40,9 @@ import {
40
40
  ChevronRight,
41
41
  Copy,
42
42
  List,
43
+ HelpCircle,
43
44
  } from 'lucide-react';
45
+ import Swal from 'sweetalert2';
44
46
  import { confirmDialog } from '../utils/ConfirmDialog';
45
47
 
46
48
  import 'react-toggle/style.css';
@@ -166,6 +168,24 @@ function GenericFormBuilder({ setting, urlParam, userProfile }) {
166
168
  * tab itself has already replaced.
167
169
  */
168
170
  const expectedDetailHashRef = useRef(null);
171
+
172
+ /**
173
+ * The `{label, detail}` the server is believed to hold, as a comparable
174
+ * string.
175
+ *
176
+ * Every authoring action on this screen — adding a question, rewording an
177
+ * option, changing a size — only moves React state. Nothing reaches the
178
+ * server until the template itself is saved, and until this ref existed
179
+ * there was no way to tell "on screen" from "stored": a tab could be closed
180
+ * on an afternoon's work and say nothing about it.
181
+ *
182
+ * Null until the first load lands. Before that there is no baseline, and
183
+ * everything would compare as changed.
184
+ */
185
+ const savedSnapshotRef = useRef(null);
186
+ const [unsavedChanges, setUnsavedChanges] = useState(false);
187
+ /** Bumped whenever the baseline moves, so the comparison below re-runs. */
188
+ const [savedBaselineTick, setSavedBaselineTick] = useState(0);
169
189
  const lockEnabled = Boolean(optimisticLock);
170
190
  const lockSchemaVersion =
171
191
  optimisticLock?.schemaVersion ?? DEFAULT_LOCK_SCHEMA_VERSION;
@@ -1395,6 +1415,20 @@ function GenericFormBuilder({ setting, urlParam, userProfile }) {
1395
1415
  (!field.options || field.options.length === 0)
1396
1416
  ) {
1397
1417
  errors.push('Please add options for the dropdown.');
1418
+ } else if (
1419
+ /**
1420
+ * A checkbox with no options renders as a bare label with nothing
1421
+ * to tick. Harmless-looking in the builder, and fatal in the field:
1422
+ * marked required, it cannot be answered, so the form it belongs to
1423
+ * can never be submitted. Dropdowns have always been held to this;
1424
+ * checkboxes were simply missed.
1425
+ */
1426
+ field.type === 'checkbox' &&
1427
+ (!field.options || field.options.length === 0)
1428
+ ) {
1429
+ errors.push(
1430
+ 'Please add at least one option for the checkbox — a checkbox with no options cannot be answered.'
1431
+ );
1398
1432
  } else if (
1399
1433
  field.type === 'table_text' &&
1400
1434
  (!field.options || field.options.length === 0)
@@ -1812,6 +1846,34 @@ function GenericFormBuilder({ setting, urlParam, userProfile }) {
1812
1846
  );
1813
1847
  };
1814
1848
 
1849
+ /** The comparable form of the two things this screen actually stores. */
1850
+ const snapshotOf = (label, detail) =>
1851
+ JSON.stringify({ label: label ?? '', detail: detail ?? [] });
1852
+
1853
+ /**
1854
+ * Re-anchor the unsaved-changes baseline on what the server now holds.
1855
+ *
1856
+ * Takes a partial: the sort endpoint writes `detail` alone and leaves the
1857
+ * label as it was, so rebuilding the whole baseline from the current
1858
+ * on-screen `data` would quietly adopt an unsaved rename as "stored".
1859
+ */
1860
+ const markSaved = ({ label, detail } = {}) => {
1861
+ const previous = savedSnapshotRef.current
1862
+ ? JSON.parse(savedSnapshotRef.current)
1863
+ : {};
1864
+
1865
+ savedSnapshotRef.current = snapshotOf(
1866
+ label === undefined ? previous.label : label,
1867
+ detail === undefined ? previous.detail : detail
1868
+ );
1869
+
1870
+ // Never `setUnsavedChanges(false)` directly: a partial save can leave
1871
+ // something else still unsaved. Re-ordering writes `detail` and not the
1872
+ // label, so declaring the screen clean here would lose a pending
1873
+ // rename. Let the one comparison decide.
1874
+ setSavedBaselineTick((tick) => tick + 1);
1875
+ };
1876
+
1815
1877
  /**
1816
1878
  * "Somebody else got here first", in the only words that help.
1817
1879
  *
@@ -1875,6 +1937,10 @@ function GenericFormBuilder({ setting, urlParam, userProfile }) {
1875
1937
 
1876
1938
  if (res.data.error === '') {
1877
1939
  await rememberDetailHash(data.id, savedDetail);
1940
+ markSaved({
1941
+ label: data.label,
1942
+ detail: savedDetail,
1943
+ });
1878
1944
 
1879
1945
  toast.success(
1880
1946
  "You have successfully updated the form's detail."
@@ -1910,7 +1976,31 @@ function GenericFormBuilder({ setting, urlParam, userProfile }) {
1910
1976
  return;
1911
1977
  }
1912
1978
 
1979
+ /**
1980
+ * Suppressing the shared handler's toast made this branch the
1981
+ * only voice the user has, so it must never be silent. It used
1982
+ * to `return` here, which turned an expired session (401
1983
+ * `Unauthenticated.`) — or any error body carrying neither
1984
+ * `errors` nor `message` — into a save that looked like it had
1985
+ * worked: nothing stored, and nothing said about it.
1986
+ */
1913
1987
  if (err?.response) {
1988
+ const status = err.response.status;
1989
+ const expired =
1990
+ status === 401 || message === 'Unauthenticated.';
1991
+
1992
+ toast.error(
1993
+ <div>
1994
+ <strong>Nothing was saved.</strong>{' '}
1995
+ {expired
1996
+ ? 'Your session has expired. Sign in again in another tab, then come back and save — your changes are still on screen.'
1997
+ : `The server refused the save${
1998
+ status ? ` (error ${status})` : ''
1999
+ }. Your changes are still on screen — try again, and report this if it keeps happening.`}
2000
+ </div>,
2001
+ { autoClose: false }
2002
+ );
2003
+
1914
2004
  return;
1915
2005
  }
1916
2006
  }
@@ -1932,6 +2022,10 @@ function GenericFormBuilder({ setting, urlParam, userProfile }) {
1932
2022
  // The version this tab is editing from. Everything the guard does
1933
2023
  // is measured against this moment.
1934
2024
  await rememberDetailHash(res.data?.id, res.data?.detail);
2025
+ markSaved({
2026
+ label: res.data?.label,
2027
+ detail: res.data?.detail,
2028
+ });
1935
2029
  } catch (err) {
1936
2030
  toast.error(`Error: ${err}`);
1937
2031
  }
@@ -1959,6 +2053,10 @@ function GenericFormBuilder({ setting, urlParam, userProfile }) {
1959
2053
  // here — but leaving the hash behind would make the next Save
1960
2054
  // report a collision this tab caused itself.
1961
2055
  await rememberDetailHash(data.id, savedDetail);
2056
+
2057
+ // Re-ordering is the one authoring action that reaches the server
2058
+ // on its own, so the unsaved-changes baseline has to follow it too.
2059
+ markSaved({ detail: savedDetail });
1962
2060
  } catch (err) {
1963
2061
  toast.error(`Error: ${err}`);
1964
2062
  }
@@ -1990,6 +2088,297 @@ function GenericFormBuilder({ setting, urlParam, userProfile }) {
1990
2088
  fetchRoles();
1991
2089
  }, []);
1992
2090
 
2091
+ /**
2092
+ * Is what is on screen different from what is stored?
2093
+ *
2094
+ * Compared against the baseline rather than tracked per-action, so it
2095
+ * cannot drift: undoing a change by hand correctly reports "all changes
2096
+ * saved" again, and no authoring path can forget to raise the flag.
2097
+ */
2098
+ useEffect(() => {
2099
+ if (savedSnapshotRef.current === null) {
2100
+ return;
2101
+ }
2102
+
2103
+ setUnsavedChanges(
2104
+ snapshotOf(data.label, data.detail) !== savedSnapshotRef.current
2105
+ );
2106
+ }, [data, savedBaselineTick]);
2107
+
2108
+ /**
2109
+ * The browser's own "leave site?" prompt, and only while there is something
2110
+ * to lose. Last line of defence for the failure this screen is prone to: a
2111
+ * question window's button reads as final, so a template can be closed in
2112
+ * the belief that its questions were filed.
2113
+ */
2114
+ useEffect(() => {
2115
+ if (!unsavedChanges) {
2116
+ return undefined;
2117
+ }
2118
+
2119
+ const warnBeforeLeaving = (event) => {
2120
+ event.preventDefault();
2121
+ event.returnValue = '';
2122
+
2123
+ return '';
2124
+ };
2125
+
2126
+ window.addEventListener('beforeunload', warnBeforeLeaving);
2127
+
2128
+ return () =>
2129
+ window.removeEventListener('beforeunload', warnBeforeLeaving);
2130
+ }, [unsavedChanges]);
2131
+
2132
+ /* ---------------------------------------------------------------- */
2133
+ /* Guided help */
2134
+ /* ---------------------------------------------------------------- */
2135
+
2136
+ /**
2137
+ * Inline SVG for the guide's section headings. SweetAlert2 takes an HTML
2138
+ * string, so the lucide components used elsewhere in this file cannot be
2139
+ * rendered here — these mirror the same icons at the same weight.
2140
+ */
2141
+ const helpIcon = (name, color) => {
2142
+ const open = `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="${color}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align: middle; display: inline-block;">`;
2143
+ const paths = {
2144
+ rocket: '<path d="M12 2c5 3 8 5 8 9a6 6 0 1 1-12 0c0-4 3-6 8-9Z"/><path d="m16 6-4 14-4-14"/><circle cx="12" cy="12" r="2"/>',
2145
+ list: '<line x1="8" x2="21" y1="6" y2="6"/><line x1="8" x2="21" y1="12" y2="12"/><line x1="8" x2="21" y1="18" y2="18"/><line x1="3" x2="3.01" y1="6" y2="6"/><line x1="3" x2="3.01" y1="12" y2="12"/><line x1="3" x2="3.01" y1="18" y2="18"/>',
2146
+ layers: '<path d="m12.83 2.18a2 2 0 0 0-1.66 0L2.6 6.08a1 1 0 0 0 0 1.83l8.58 3.91a2 2 0 0 0 1.66 0l8.58-3.9a1 1 0 0 0 0-1.83Z"/><path d="m22 12.5-9.17 4.16a2 2 0 0 1-1.66 0L2 12.5"/>',
2147
+ type: '<polyline points="4 7 4 4 20 4 20 7"/><line x1="9" x2="15" y1="20" y2="20"/><line x1="12" x2="12" y1="4" y2="20"/>',
2148
+ settings:
2149
+ '<path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"/><circle cx="12" cy="12" r="3"/>',
2150
+ table: '<path d="M12 3v18"/><rect width="18" height="18" x="3" y="3" rx="2"/><path d="M3 9h18"/><path d="M3 15h18"/>',
2151
+ branch: '<line x1="6" x2="6" y1="3" y2="15"/><circle cx="18" cy="6" r="3"/><circle cx="6" cy="18" r="3"/><path d="M18 9a9 9 0 0 1-9 9"/>',
2152
+ flag: '<path d="M4 15s1-1 4-1 5 2 8 2 4-1 4-1V3s-1 1-4 1-5-2-8-2-4 1-4 1z"/><line x1="4" x2="4" y1="22" y2="15"/>',
2153
+ save: '<path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"/><polyline points="17 21 17 13 7 13 7 21"/><polyline points="7 3 7 8 15 8"/>',
2154
+ help: '<circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><path d="M12 17h.01"/>',
2155
+ };
2156
+ return `${open}${paths[name] || paths.help}</svg>`;
2157
+ };
2158
+
2159
+ /** One heading style for every section of the guide. */
2160
+ const helpHeading = (icon, color, title) =>
2161
+ `<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;">${helpIcon(
2162
+ icon,
2163
+ color
2164
+ )}${title}</h3>`;
2165
+
2166
+ /** A tinted panel with a bulleted body, optionally titled. */
2167
+ const helpPanel = ({ bg, border, text, title, items }) => `
2168
+ <div style="background: ${bg}; border: 1px solid ${border}; border-radius: 8px; padding: 12px; margin-bottom: 12px;">
2169
+ ${
2170
+ title
2171
+ ? `<h4 style="color: ${border}; margin: 0 0 8px 0; font-size: 14px;">${title}</h4>`
2172
+ : ''
2173
+ }
2174
+ <ul style="margin: 0; padding-left: 16px; color: ${text};">${items
2175
+ .map((item) => `<li>${item}</li>`)
2176
+ .join('')}</ul>
2177
+ </div>`;
2178
+
2179
+ /**
2180
+ * The end-user guide to building a template. Sections that describe an
2181
+ * opt-in feature are only included when this app actually switched that
2182
+ * feature on, so nobody is told about a control they will never see.
2183
+ */
2184
+ const showGuidedHelp = () => {
2185
+ const sections = [
2186
+ helpHeading('rocket', '#3b82f6', 'What this screen is') +
2187
+ helpPanel({
2188
+ bg: '#f0f9ff',
2189
+ border: '#3b82f6',
2190
+ text: '#1e3a8a',
2191
+ items: [
2192
+ 'This is the list of questions people will answer — for example a client review checklist',
2193
+ '<strong>Edit</strong> shows one compact row per question, with a summary of how it is set up',
2194
+ '<strong>Preview</strong> shows the form the way the person filling it in will see it',
2195
+ '<strong>Sections</strong> shows a strip of shortcuts, one per section; click one to jump to it',
2196
+ 'Once there are more than five questions, a search box appears — matching questions stay bright and the rest dim',
2197
+ ],
2198
+ }),
2199
+
2200
+ helpHeading('list', '#10b981', 'Adding and arranging questions') +
2201
+ helpPanel({
2202
+ bg: '#ecfdf5',
2203
+ border: '#10b981',
2204
+ text: '#065f46',
2205
+ items: dynamicFields
2206
+ ? [
2207
+ 'This template builds its own questions automatically, so there is no "Add Field" button',
2208
+ 'Use the pencil to adjust a question, and the grip handle on the left to drag it into a new position',
2209
+ ]
2210
+ : [
2211
+ '<strong>Add Field</strong> (bottom right) opens the question window',
2212
+ 'The pencil edits a question, the copy icon duplicates it, the bin deletes it',
2213
+ 'Duplicating a Section copies the section and every question inside it',
2214
+ 'Drag the grip handle on the left to move a question; the up/down arrows let you type an exact position instead',
2215
+ 'Deleting a question also clears any "show only when" rule that pointed at it, and tells you how many were cleared',
2216
+ ],
2217
+ }),
2218
+
2219
+ helpHeading('layers', '#7c3aed', 'Sections and headings') +
2220
+ helpPanel({
2221
+ bg: '#f5f3ff',
2222
+ border: '#7c3aed',
2223
+ text: '#5b21b6',
2224
+ items: [
2225
+ 'Add a question of type <strong>Section</strong> to start a new part of the form',
2226
+ 'Everything after a Section belongs to it, until the next Section starts',
2227
+ 'Questions before the first Section are listed under "Before first section"',
2228
+ 'Click the arrow on a Section row to collapse or expand it while you work',
2229
+ '<strong>Heading</strong> is simpler — just a title on the page, with no grouping',
2230
+ '<strong>Plain Text</strong> is a block of formatted wording, for instructions or a declaration',
2231
+ ],
2232
+ }),
2233
+
2234
+ helpHeading('type', '#0891b2', 'The kinds of question you can add') +
2235
+ helpPanel({
2236
+ bg: '#ecfeff',
2237
+ border: '#0891b2',
2238
+ text: '#155e75',
2239
+ items: [
2240
+ '<strong>Typed in:</strong> Text (one line), Textarea (several lines), Number',
2241
+ '<strong>Chosen from a list:</strong> Dropdown, Checkbox, Toggle (a yes/no switch)',
2242
+ '<strong>Dates and times:</strong> Date, Date &amp; Time, Time',
2243
+ '<strong>Attached or drawn:</strong> File, Image, Video Upload, Signature, Canvas',
2244
+ '<strong>Grids:</strong> Table (Radio) and Table (Custom) — see below',
2245
+ '<strong>Layout only:</strong> Section, Heading, Plain Text',
2246
+ `<strong>Filled from your system:</strong> Dynamic Data pulls in a detail the system already holds${
2247
+ dynamicDropdowns && dynamicDropdowns.length > 0
2248
+ ? ', and Dynamic Dropdown offers a list your system maintains'
2249
+ : ''
2250
+ }`,
2251
+ ],
2252
+ }),
2253
+
2254
+ helpHeading('settings', '#f59e0b', 'Settings on each question') +
2255
+ helpPanel({
2256
+ bg: '#fffbeb',
2257
+ border: '#f59e0b',
2258
+ text: '#92400e',
2259
+ items: [
2260
+ '<strong>Label</strong> is the wording people read — write it as the question you want answered',
2261
+ '<strong>Required?</strong> set to Yes means the form cannot be submitted without an answer; required questions show an asterisk',
2262
+ '<strong>Size</strong> is how wide the question sits on the page: Full for its own row, Half or Quarter to share a row',
2263
+ '<strong>Height Size</strong> on a Textarea chooses a small, normal or large writing box',
2264
+ '<strong>Single or Bulk Image?</strong> decides whether one photo or several can be attached',
2265
+ '<strong>Associated Role?</strong> on a Signature says whose signature it is',
2266
+ 'There is no help text, placeholder or preset answer — put any wording people need into the label, or a Plain Text block above',
2267
+ ],
2268
+ }),
2269
+
2270
+ helpHeading('list', '#059669', 'Answers for Dropdown and Checkbox') +
2271
+ helpPanel({
2272
+ bg: '#f0fdf4',
2273
+ border: '#059669',
2274
+ text: '#166534',
2275
+ items: [
2276
+ 'Type the answer in the box at the bottom of the Options list and press Enter, or click <strong>+ Add</strong>',
2277
+ 'Click straight into an existing answer to reword it; the bin removes it',
2278
+ 'Two answers cannot share the same wording',
2279
+ 'Rewording an answer can unhook rules that were pointing at it — check any "show only when" rule that used it',
2280
+ ],
2281
+ }),
2282
+
2283
+ helpHeading('table', '#be185d', 'Grid questions') +
2284
+ helpPanel({
2285
+ bg: '#fdf2f8',
2286
+ border: '#be185d',
2287
+ text: '#9d174d',
2288
+ items: [
2289
+ '<strong>Table (Radio)</strong> needs both Columns and Rows — the rows are the things being checked and the columns are the possible answers',
2290
+ '<strong>Table (Custom)</strong> needs Columns only, and gives one row of boxes to fill in',
2291
+ 'Add a column or row by typing it in and pressing Enter, or clicking Add',
2292
+ 'The rows are fixed when you build the template — the person filling in the form cannot add more',
2293
+ ],
2294
+ }),
2295
+
2296
+ helpHeading(
2297
+ 'branch',
2298
+ '#4f46e5',
2299
+ 'Showing a question only when it applies'
2300
+ ) +
2301
+ helpPanel({
2302
+ bg: '#eef2ff',
2303
+ border: '#4f46e5',
2304
+ text: '#3730a3',
2305
+ items: [
2306
+ 'Every question window ends with <strong>Conditional Field Display Criteria</strong>',
2307
+ 'Pick the earlier question it depends on, then either <strong>is</strong> with a value, or <strong>is filled in</strong>',
2308
+ 'Only questions that come <em>before</em> this one can be used, and only ones that collect an answer',
2309
+ 'A line at the bottom spells the rule out, e.g. "This field will only show when Advice type is Insurance"',
2310
+ 'Leave it blank to always show the question, or use <strong>Clear rule</strong> to remove one',
2311
+ 'Questions with a rule are marked "Conditional" in the list',
2312
+ ],
2313
+ }),
2314
+ ];
2315
+
2316
+ if (outstandingItems?.enabled) {
2317
+ sections.push(
2318
+ helpHeading('flag', '#d97706', 'Answers that raise a follow-up') +
2319
+ helpPanel({
2320
+ bg: '#fef3c7',
2321
+ border: '#d97706',
2322
+ text: '#92400e',
2323
+ items: [
2324
+ 'On questions that offer answers, each answer has an <strong>Outstanding</strong> switch',
2325
+ 'Turn it on for the answer that means "this needs following up", and the system raises an outstanding item when someone gives that answer',
2326
+ 'Only one answer per question can be marked',
2327
+ 'Leave every switch off to say this question never raises a follow-up',
2328
+ 'If an amber warning appears, the answer the old rule pointed at no longer exists — set the switch again on the right answer',
2329
+ ],
2330
+ })
2331
+ );
2332
+ }
2333
+
2334
+ sections.push(
2335
+ helpHeading('save', '#dc2626', 'Saving your work') +
2336
+ helpPanel({
2337
+ bg: '#fef2f2',
2338
+ border: '#dc2626',
2339
+ text: '#991b1b',
2340
+ items: [
2341
+ '<strong>Apply</strong> inside a question window only adds it to the list on screen — nothing is stored yet',
2342
+ 'To store the template, click <strong>Save Template</strong> (bottom right)',
2343
+ 'The label beside those buttons reads <strong>Unsaved changes</strong> until you do, and the browser warns you if you try to leave',
2344
+ '<strong>Edit Form</strong> is only for renaming the template',
2345
+ 'Moving a question saves the whole template straight away, so the order is never lost',
2346
+ 'If someone else changed this template while you had it open, you are told nothing was saved — reload the page before saving again, or your changes would undo theirs',
2347
+ ],
2348
+ })
2349
+ );
2350
+
2351
+ Swal.fire({
2352
+ title: `<div style="display: flex; align-items: center; justify-content: center; gap: 8px;">${helpIcon(
2353
+ 'help',
2354
+ '#3b82f6'
2355
+ )}Form Builder Guide</div>`,
2356
+ html: `<div style="text-align: left; font-size: 13px; line-height: 1.5; color: #374151; max-height: 70vh; overflow-y: auto;">${sections.join(
2357
+ '<div style="height: 12px;"></div>'
2358
+ )}</div>`,
2359
+ width: 800,
2360
+ confirmButtonText: 'Close',
2361
+ confirmButtonColor: '#6b7280',
2362
+ customClass: {
2363
+ popup: 'comprehensive-help-modal',
2364
+ htmlContainer: 'help-content-scrollable',
2365
+ },
2366
+ });
2367
+ };
2368
+
2369
+ // Show the guide once, the first time someone opens the builder.
2370
+ useEffect(() => {
2371
+ if (localStorage.getItem('formBuilder_hasSeenHelp')) return;
2372
+
2373
+ const timer = setTimeout(() => {
2374
+ showGuidedHelp();
2375
+ localStorage.setItem('formBuilder_hasSeenHelp', 'true');
2376
+ }, 500);
2377
+
2378
+ return () => clearTimeout(timer);
2379
+ // eslint-disable-next-line react-hooks/exhaustive-deps
2380
+ }, []);
2381
+
1993
2382
  return (
1994
2383
  <div className={modalShow || modalFormShow ? styles.modalOpen : ''}>
1995
2384
  <div className={styles.grid}>
@@ -2081,6 +2470,15 @@ function GenericFormBuilder({ setting, urlParam, userProfile }) {
2081
2470
  ).length}{' '}
2082
2471
  sections
2083
2472
  </span>
2473
+ <button
2474
+ type="button"
2475
+ className={styles.toolBtn}
2476
+ onClick={showGuidedHelp}
2477
+ data-tooltip-id="action-tooltip"
2478
+ data-tooltip-content="How to build this form"
2479
+ >
2480
+ <HelpCircle size={15} /> Help
2481
+ </button>
2084
2482
  <button
2085
2483
  className={`${styles.toolBtn} ${
2086
2484
  showOutline ? styles.toolBtnOn : ''
@@ -2307,6 +2705,20 @@ function GenericFormBuilder({ setting, urlParam, userProfile }) {
2307
2705
  )}
2308
2706
  </div>
2309
2707
  <div className={styles.polActions}>
2708
+ {/* The only honest answer to "have my changes been
2709
+ stored?". Everything else on this screen edits a
2710
+ copy held in the browser. */}
2711
+ <span
2712
+ className={`${styles.saveState} ${
2713
+ unsavedChanges
2714
+ ? styles.saveStateDirty
2715
+ : styles.saveStateClean
2716
+ }`}
2717
+ >
2718
+ {unsavedChanges
2719
+ ? 'Unsaved changes'
2720
+ : 'All changes saved'}
2721
+ </span>
2310
2722
  {/* Nothing to author on a dynamic template, but
2311
2723
  Edit Form stays so it can still be renamed. */}
2312
2724
  {!dynamicFields && (
@@ -2323,6 +2735,19 @@ function GenericFormBuilder({ setting, urlParam, userProfile }) {
2323
2735
  >
2324
2736
  Edit Form
2325
2737
  </button>
2738
+ {/* Was reachable only through Edit Form — a window
2739
+ titled "Update Form Detail" showing nothing but
2740
+ the label, so the action that stores the
2741
+ questions looked like the one that renames the
2742
+ form. It belongs out here, beside the work. */}
2743
+ <button
2744
+ className={`${styles.btn} ${styles.btnPrimary}`}
2745
+ onClick={handleSubmit}
2746
+ data-tooltip-id="system-tooltip"
2747
+ data-tooltip-content="Store this template. Question windows only stage changes on screen."
2748
+ >
2749
+ Save Template
2750
+ </button>
2326
2751
  </div>
2327
2752
  </div>
2328
2753
  </div>
@@ -3140,11 +3565,23 @@ function GenericFormBuilder({ setting, urlParam, userProfile }) {
3140
3565
  <div
3141
3566
  className={`${styles.formItem} ${styles.fwItem} ${styles.lastItem}`}
3142
3567
  >
3568
+ {/* This window has never stored
3569
+ anything — it edits the copy held in
3570
+ the browser. Calling its button
3571
+ "Save" was the whole reason people
3572
+ left the screen believing their work
3573
+ had been filed. */}
3574
+ <p className={styles.applyHint}>
3575
+ Adds this question to the list on
3576
+ screen. Use{' '}
3577
+ <strong>Save Template</strong> to
3578
+ store it.
3579
+ </p>
3143
3580
  <button
3144
3581
  className={`${styles.btn}`}
3145
3582
  onClick={handleSaveField}
3146
3583
  >
3147
- Save
3584
+ Apply
3148
3585
  </button>
3149
3586
  </div>
3150
3587
  </form>
@@ -1752,224 +1752,56 @@ const GenericReportImproved = ({
1752
1752
  return icons[iconName] || icons.chart;
1753
1753
  };
1754
1754
 
1755
- // Show comprehensive help guide
1756
- 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) =>
1757
1796
  Swal.fire({
1758
1797
  title: `<div style="display: flex; align-items: center; justify-content: center; gap: 8px;">${getIcon(
1759
- 'chart',
1798
+ titleIcon,
1760
1799
  '#3b82f6',
1761
1800
  24
1762
- )}Complete Report Builder Guide</div>`,
1763
- html: `
1764
- <div style="text-align: left; font-size: 13px; line-height: 1.5; color: #374151; max-height: 70vh; overflow-y: auto;">
1765
-
1766
- <!-- Quick Start Section -->
1767
- <div style="margin-bottom: 24px;">
1768
- <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;">
1769
- ${getIcon('rocket', '#3b82f6', 16)}
1770
- Getting Started
1771
- </h3>
1772
-
1773
- <div style="background: #f0f9ff; border-left: 4px solid #3b82f6; padding: 12px; margin-bottom: 12px;">
1774
- <h4 style="color: #1e40af; margin: 0 0 8px 0; font-size: 14px; display: flex; align-items: center; gap: 6px;">
1775
- ${getIcon('file', '#1e40af', 14)}
1776
- Option 1: Use Saved Reports (Recommended)
1777
- </h4>
1778
- <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>
1779
- <p style="margin: 0; font-style: italic; color: #1e40af;">Perfect for beginners and quick results!</p>
1780
- </div>
1781
-
1782
- <div style="background: #f9fafb; border-left: 4px solid #6b7280; padding: 12px;">
1783
- <h4 style="color: #374151; margin: 0 0 8px 0; font-size: 14px; display: flex; align-items: center; gap: 6px;">
1784
- ${getIcon('cogs', '#374151', 14)}
1785
- Option 2: Build Custom Report
1786
- </h4>
1787
- <p style="margin: 0;">Follow the 5-step wizard below to create exactly what you need.</p>
1788
- </div>
1789
- </div>
1790
-
1791
- <!-- Step-by-Step Guide -->
1792
- <div style="margin-bottom: 24px;">
1793
- <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;">
1794
- ${getIcon('list', '#10b981', 16)}
1795
- 5-Step Report Creation
1796
- </h3>
1797
-
1798
- <!-- Step 1 -->
1799
- <div style="background: #ecfdf5; border: 1px solid #10b981; border-radius: 8px; padding: 12px; margin-bottom: 12px;">
1800
- <h4 style="color: #047857; margin: 0 0 8px 0; font-size: 14px; display: flex; align-items: center; gap: 6px;">
1801
- ${getIcon('database', '#047857', 14)}
1802
- Step 1: Choose Your Data Source
1803
- </h4>
1804
- <ul style="margin: 0; padding-left: 16px; color: #065f46;">
1805
- <li>Select the main table (e.g., Clients, Orders, Invoices)</li>
1806
- <li>This becomes the foundation of your report</li>
1807
- <li>Look for the table icon to identify data types</li>
1808
- </ul>
1809
- </div>
1810
-
1811
- <!-- Step 2 -->
1812
- <div style="background: #eff6ff; border: 1px solid #3b82f6; border-radius: 8px; padding: 12px; margin-bottom: 12px;">
1813
- <h4 style="color: #1e40af; margin: 0 0 8px 0; font-size: 14px; display: flex; align-items: center; gap: 6px;">
1814
- ${getIcon('columns', '#1e40af', 14)}
1815
- Step 2: Select Columns
1816
- </h4>
1817
- <ul style="margin: 0; padding-left: 16px; color: #1e3a8a;">
1818
- <li>Check the boxes for data you want to see</li>
1819
- <li>Drag columns to reorder them</li>
1820
- <li>Use "Select All" for comprehensive reports</li>
1821
- <li>Preview shows how your data will look</li>
1822
- </ul>
1823
- </div>
1824
-
1825
- <!-- Step 3 -->
1826
- <div style="background: #fef3c7; border: 1px solid #f59e0b; border-radius: 8px; padding: 12px; margin-bottom: 12px;">
1827
- <h4 style="color: #92400e; margin: 0 0 8px 0; font-size: 14px; display: flex; align-items: center; gap: 6px;">
1828
- ${getIcon('link', '#92400e', 14)}
1829
- Step 3: Add Relationships (Optional)
1830
- </h4>
1831
- <ul style="margin: 0; padding-left: 16px; color: #92400e;">
1832
- <li>Connect related data (e.g., add customer names to invoices)</li>
1833
- <li>We suggest common relationships automatically</li>
1834
- <li>Click "Add Relationship" to connect more tables</li>
1835
- <li>This enriches your report with additional context</li>
1836
- </ul>
1837
- </div>
1838
-
1839
- <!-- Step 4 -->
1840
- <div style="background: #fdf2f8; border: 1px solid #ec4899; border-radius: 8px; padding: 12px; margin-bottom: 12px;">
1841
- <h4 style="color: #be185d; margin: 0 0 8px 0; font-size: 14px; display: flex; align-items: center; gap: 6px;">
1842
- ${getIcon('filter', '#be185d', 14)}
1843
- Step 4: Apply Filters (Optional)
1844
- </h4>
1845
- <ul style="margin: 0; padding-left: 16px; color: #be185d;">
1846
- <li>Narrow down results (e.g., "Active clients only")</li>
1847
- <li>Use AND/OR logic for complex conditions</li>
1848
- <li>Group filters for advanced filtering</li>
1849
- <li>Great for date ranges, status filters, etc.</li>
1850
- </ul>
1851
- </div>
1852
-
1853
- <!-- Step 5 -->
1854
- <div style="background: #f3e8ff; border: 1px solid #8b5cf6; border-radius: 8px; padding: 12px;">
1855
- <h4 style="color: #7c3aed; margin: 0 0 8px 0; font-size: 14px; display: flex; align-items: center; gap: 6px;">
1856
- ${getIcon('download', '#7c3aed', 14)}
1857
- Step 5: Generate & Export
1858
- </h4>
1859
- <ul style="margin: 0; padding-left: 16px; color: #7c3aed;">
1860
- <li>Click "Generate Report" to see your data</li>
1861
- <li>Review and refine as needed</li>
1862
- <li>Export as Excel, CSV, or PDF</li>
1863
- <li>Save the report template for future use</li>
1864
- </ul>
1865
- </div>
1866
- </div>
1867
-
1868
- <!-- Pro Tips Section -->
1869
- <div style="margin-bottom: 20px;">
1870
- <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;">
1871
- ${getIcon('star', '#f59e0b', 16)}
1872
- Pro Tips
1873
- </h3>
1874
-
1875
- <div style="background: #fffbeb; border: 1px solid #f59e0b; border-radius: 8px; padding: 12px;">
1876
- <ul style="margin: 0; padding-left: 16px; color: #92400e;">
1877
- <li><strong>Start Simple:</strong> Begin with basic reports, then add complexity</li>
1878
- <li><strong>Use Previews:</strong> Check the preview before generating large reports</li>
1879
- <li><strong>Save Templates:</strong> Save frequently used reports as templates</li>
1880
- <li><strong>Date Filters:</strong> Use date ranges for better performance</li>
1881
- <li><strong>Column Limits:</strong> Too many columns can slow down reports</li>
1882
- </ul>
1883
- </div>
1884
- </div>
1885
-
1886
- <!-- Common Use Cases -->
1887
- <div style="margin-bottom: 20px;">
1888
- <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;">
1889
- ${getIcon('lightbulb', '#10b981', 16)}
1890
- Common Report Examples
1891
- </h3>
1892
-
1893
- <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 8px;">
1894
- <div style="background: #f0fdf4; border: 1px solid #22c55e; border-radius: 6px; padding: 8px;">
1895
- <strong style="color: #15803d;">${getIcon(
1896
- 'users',
1897
- '#15803d',
1898
- 12
1899
- )}Client List:</strong>
1900
- <br><small style="color: #16a34a;">Clients table + Contact details</small>
1901
- </div>
1902
- <div style="background: #eff6ff; border: 1px solid #3b82f6; border-radius: 6px; padding: 8px;">
1903
- <strong style="color: #1d4ed8;">${getIcon(
1904
- 'cart',
1905
- '#1d4ed8',
1906
- 12
1907
- )}Recent Orders:</strong>
1908
- <br><small style="color: #2563eb;">Orders + Customer names + Date filter</small>
1909
- </div>
1910
- <div style="background: #fef3c7; border: 1px solid #f59e0b; border-radius: 6px; padding: 8px;">
1911
- <strong style="color: #d97706;">${getIcon(
1912
- 'money',
1913
- '#d97706',
1914
- 12
1915
- )}Financial Summary:</strong>
1916
- <br><small style="color: #f59e0b;">Invoices + Payments + Date grouping</small>
1917
- </div>
1918
- <div style="background: #fdf2f8; border: 1px solid #ec4899; border-radius: 6px; padding: 8px;">
1919
- <strong style="color: #db2777;">${getIcon(
1920
- 'tasks',
1921
- '#db2777',
1922
- 12
1923
- )}Active Projects:</strong>
1924
- <br><small style="color: #ec4899;">Projects + Status filter + Team members</small>
1925
- </div>
1926
- </div>
1927
- </div>
1928
-
1929
- <!-- Troubleshooting -->
1930
- <div>
1931
- <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;">
1932
- ${getIcon('settings', '#dc2626', 16)}
1933
- Common Issues
1934
- </h3>
1935
-
1936
- <div style="background: #fef2f2; border: 1px solid #dc2626; border-radius: 8px; padding: 12px;">
1937
- <div style="margin-bottom: 8px;">
1938
- <strong style="color: #dc2626;">${getIcon(
1939
- 'clock',
1940
- '#dc2626',
1941
- 12
1942
- )}Report too slow?</strong>
1943
- <span style="color: #991b1b;"> Add date filters or reduce columns</span>
1944
- </div>
1945
- <div style="margin-bottom: 8px;">
1946
- <strong style="color: #dc2626;">${getIcon(
1947
- 'question',
1948
- '#dc2626',
1949
- 12
1950
- )}Missing data?</strong>
1951
- <span style="color: #991b1b;"> Check relationships and join conditions</span>
1952
- </div>
1953
- <div style="margin-bottom: 8px;">
1954
- <strong style="color: #dc2626;">${getIcon(
1955
- 'warning',
1956
- '#dc2626',
1957
- 12
1958
- )}Export failing?</strong>
1959
- <span style="color: #991b1b;"> Try smaller date ranges or fewer rows</span>
1960
- </div>
1961
- <div>
1962
- <strong style="color: #dc2626;">${getIcon(
1963
- 'eye',
1964
- '#dc2626',
1965
- 12
1966
- )}No data showing?</strong>
1967
- <span style="color: #991b1b;"> Review your filter conditions</span>
1968
- </div>
1969
- </div>
1970
- </div>
1971
- </div>
1972
- `,
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>`,
1973
1805
  width: 800,
1974
1806
  confirmButtonText: 'Close',
1975
1807
  confirmButtonColor: '#6b7280',
@@ -1980,7 +1812,324 @@ const GenericReportImproved = ({
1980
1812
  htmlContainer: 'help-content-scrollable',
1981
1813
  },
1982
1814
  });
1983
- };
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();
1984
2133
 
1985
2134
  // Initialize component
1986
2135
  useEffect(() => {
@@ -1994,12 +2143,17 @@ const GenericReportImproved = ({
1994
2143
 
1995
2144
  fetchSavedReports();
1996
2145
 
1997
- // Show help on first load
1998
- 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);
1999
2153
  if (!hasSeenHelp) {
2000
2154
  setTimeout(() => {
2001
2155
  showGuidedHelp();
2002
- localStorage.setItem('reportBuilder_hasSeenHelp', 'true');
2156
+ localStorage.setItem(helpSeenKey, 'true');
2003
2157
  }, 500);
2004
2158
  }
2005
2159
  }, [tableUrl, reportsUrl, semanticStatus]); // Re-fetch when URLs change
@@ -643,7 +643,8 @@ select:not(:placeholder-shown) + .fi__span {
643
643
  overflow: visible;
644
644
  position: relative;
645
645
  z-index: 100;
646
- box-shadow: 0 20px 60px rgba(0, 0, 0, 0.15),
646
+ box-shadow:
647
+ 0 20px 60px rgba(0, 0, 0, 0.15),
647
648
  0 0 0 1px rgba(var(--primary-rgb), 0.06);
648
649
  }
649
650
 
@@ -786,9 +787,6 @@ select:not(:placeholder-shown) + .fi__span {
786
787
 
787
788
  /* Adding btn class */
788
789
 
789
-
790
-
791
-
792
790
  /* Floating action buttons */
793
791
  .polActions {
794
792
  position: fixed;
@@ -801,9 +799,11 @@ select:not(:placeholder-shown) + .fi__span {
801
799
  background: rgba(255, 255, 255, 0.95);
802
800
  padding: 10px 14px;
803
801
  border-radius: 12px;
804
- box-shadow: 0 4px 20px rgba(0, 0, 0, 0.12),
802
+ box-shadow:
803
+ 0 4px 20px rgba(0, 0, 0, 0.12),
805
804
  0 0 0 1px rgba(var(--primary-rgb), 0.08);
806
805
  backdrop-filter: blur(8px);
806
+ align-items: center;
807
807
 
808
808
  button {
809
809
  display: block;
@@ -814,6 +814,50 @@ select:not(:placeholder-shown) + .fi__span {
814
814
  }
815
815
  }
816
816
 
817
+ /* Stored or not — the one thing this screen never used to say out loud. */
818
+ .saveState {
819
+ display: inline-flex;
820
+ align-items: center;
821
+ gap: 6px;
822
+ padding: 0 6px;
823
+ font-size: 0.8rem;
824
+ font-weight: 600;
825
+ white-space: nowrap;
826
+
827
+ &::before {
828
+ content: '';
829
+ width: 8px;
830
+ height: 8px;
831
+ border-radius: 50%;
832
+ background: currentColor;
833
+ }
834
+ }
835
+
836
+ .saveStateDirty {
837
+ color: #b45309;
838
+ }
839
+
840
+ .saveStateClean {
841
+ color: #15803d;
842
+ }
843
+
844
+ /* The save that actually writes, weighted so it reads as the end of the job. */
845
+ .btnPrimary {
846
+ box-shadow: 0 0 0 2px rgba(var(--primary-rgb), 0.35);
847
+ }
848
+
849
+ /* Sits under the question window's Apply, where the misunderstanding happens. */
850
+ .applyHint {
851
+ flex: 1;
852
+ align-self: center;
853
+ margin: 0;
854
+ padding-right: 1rem;
855
+ font-size: 0.78rem;
856
+ line-height: 1.35;
857
+ color: var(--text-color-light, #6b7280);
858
+ text-align: left;
859
+ }
860
+
817
861
  .fwItem {
818
862
  flex-basis: 100%;
819
863
  }
@@ -1337,6 +1381,12 @@ select:not(:placeholder-shown) + .fi__span {
1337
1381
  .dragHandle {
1338
1382
  color: var(--primary-color) !important;
1339
1383
  cursor: grab;
1384
+ /* Same reason as .cGrip: on iPadOS the browser treats a press-and-move on
1385
+ this handle as a scroll unless the element opts out, so the drag never
1386
+ activates and reordering is silently unavailable. */
1387
+ touch-action: none;
1388
+ -webkit-user-select: none;
1389
+ user-select: none;
1340
1390
  padding: 4px;
1341
1391
  border-radius: 6px;
1342
1392
  background: white;
@@ -2047,7 +2097,8 @@ select:not(:placeholder-shown) + .fi__span {
2047
2097
  border-radius: 12px;
2048
2098
  padding: 1.25rem 1.5rem;
2049
2099
  width: 280px;
2050
- box-shadow: 0 16px 48px rgba(0, 0, 0, 0.18),
2100
+ box-shadow:
2101
+ 0 16px 48px rgba(0, 0, 0, 0.18),
2051
2102
  0 0 0 1px rgba(var(--primary-rgb), 0.08);
2052
2103
 
2053
2104
  h3 {
@@ -2472,7 +2523,6 @@ select:not(:placeholder-shown) + .fi__span {
2472
2523
  margin-right: -1.15rem !important;
2473
2524
  }
2474
2525
 
2475
-
2476
2526
  /* The header and search span both columns; only the rail and canvas sit
2477
2527
  side by side. */
2478
2528
 
@@ -2622,9 +2672,6 @@ select:not(:placeholder-shown) + .fi__span {
2622
2672
 
2623
2673
  /* ---- compact rows ---- */
2624
2674
 
2625
-
2626
-
2627
-
2628
2675
  .duplicateBtn {
2629
2676
  display: flex;
2630
2677
  align-items: center;
@@ -2705,6 +2752,12 @@ select:not(:placeholder-shown) + .fi__span {
2705
2752
  align-items: center;
2706
2753
  color: rgba(var(--paragraph-rgb), 0.4);
2707
2754
  cursor: grab;
2755
+ /* dnd-kit's PointerSensor cannot start a drag from an element the browser
2756
+ has already claimed for scrolling. Without this the grip does nothing at
2757
+ all on a touch screen — the page just scrolls under the finger. */
2758
+ touch-action: none;
2759
+ -webkit-user-select: none;
2760
+ user-select: none;
2708
2761
 
2709
2762
  &:active {
2710
2763
  cursor: grabbing;