@gov-cy/govcy-express-services 1.2.0 → 1.3.0-alpha

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.
@@ -0,0 +1,460 @@
1
+ import { getPageConfigData } from "../utils/govcyLoadConfigData.mjs";
2
+ import { populateFormData } from "../utils/govcyFormHandling.mjs";
3
+ import * as govcyResources from "../resources/govcyResources.mjs";
4
+ import * as dataLayer from "../utils/govcyDataLayer.mjs";
5
+ import { logger } from "../utils/govcyLogger.mjs";
6
+ import { evaluatePageConditions } from "../utils/govcyExpressions.mjs";
7
+ import { handleMiddlewareError } from "../utils/govcyUtils.mjs";
8
+ import { getFormData } from "../utils/govcyFormHandling.mjs";
9
+ import { validateFormElements } from "../utils/govcyValidator.mjs";
10
+ import { tempSaveIfConfigured } from "../utils/govcyTempSave.mjs";
11
+ import nunjucks from "nunjucks";
12
+
13
+ /**
14
+ * Shared builder for add/edit item pages
15
+ * @param {Object} req
16
+ * @param {Object} res
17
+ * @param {Object} next
18
+ * @param {Object} initialData - prefilled form data ({} for add, object for edit)
19
+ * @param {String} actionUrl - form action URL
20
+ * @param {String} mode - add or edit
21
+ * @param {Number|null} index - index of the item being edited (null for add)
22
+ */
23
+ function multiplePageBuilder(req, res, next, initialData, actionUrl, mode, index = null) {
24
+
25
+ // Extract siteId and pageUrl from request
26
+ let { siteId, pageUrl } = req.params;
27
+
28
+ // get service data
29
+ let serviceCopy = req.serviceData;
30
+
31
+ // 🔍 Find the page by pageUrl
32
+ const page = getPageConfigData(serviceCopy, pageUrl);
33
+
34
+ // --- MultipleThings sanity checks ---
35
+ const mtConfig = page.multipleThings;
36
+ if (!mtConfig) {
37
+ logger.debug(`🚨 multipleThings config not found in page config for ${siteId}/${pageUrl}`, req);
38
+ return handleMiddlewareError(`🚨 multipleThings config not found in page config for ${siteId}/${pageUrl}`, 404, next);
39
+ // return next(new Error(`🚨 multipleThings config not found in page config for ${siteId}/${pageUrl}`));
40
+ }
41
+ if (!mtConfig.listPage || !mtConfig.listPage.title) {
42
+ logger.debug(`🚨 multipleThings.listPage.title is required for ${siteId}/${pageUrl}`, req);
43
+ return handleMiddlewareError(`🚨 multipleThings.listPage.title is required for ${siteId}/${pageUrl}`, 404, next);
44
+ }
45
+ if (!mtConfig.itemTitleTemplate || !mtConfig.min === undefined || !mtConfig.min === null || !mtConfig.max) {
46
+ logger.debug(`🚨 multipleThings.itemTitleTemplate, .min and .max are required for ${siteId}/${pageUrl}`, req);
47
+ return handleMiddlewareError(`🚨 multipleThings.itemTitleTemplate, .min and .max are required for ${siteId}/${pageUrl}`, 404, next);
48
+ }
49
+
50
+ // Deep copy pageTemplate to avoid modifying the original
51
+ const pageTemplateCopy = JSON.parse(JSON.stringify(page.pageTemplate));
52
+
53
+ // ----- Conditional logic comes here
54
+ // Check if the page has conditions and apply logic
55
+ const result = evaluatePageConditions(page, req.session, req.params.siteId, req);
56
+ if (result.result === false) {
57
+ return res.redirect(`/${req.params.siteId}/${result.redirect}`);
58
+ }
59
+
60
+ // Change the title and H1 to append "Add" or "Change" suffix
61
+ const suffix =
62
+ mode === "add"
63
+ ? govcyResources.staticResources.text.multipleThingsAddSuffix
64
+ : govcyResources.staticResources.text.multipleThingsEditSuffix;
65
+
66
+ // Append suffix to page title
67
+ if (typeof page?.pageData?.title === "object") {
68
+ for (const lang of Object.keys(page.pageData.title)) {
69
+ page.pageData.title[lang] += ` ${suffix[lang] || ""}`;
70
+ }
71
+ }
72
+
73
+ const mainSection = pageTemplateCopy.sections.find(sec => sec.name === "main");
74
+ if (mainSection && Array.isArray(mainSection.elements)) {
75
+ // Find the form element inside main
76
+ const formEl = mainSection.elements.find(el => el.element === "form");
77
+
78
+ if (formEl && Array.isArray(formEl.params?.elements)) {
79
+ // Find the H1 textElement inside the form
80
+ const h1Element = formEl.params.elements.find(
81
+ el => el.element === "textElement" && el.params?.type === "h1"
82
+ );
83
+
84
+ if (h1Element && h1Element.params?.text) {
85
+ // Append the suffix based on mode
86
+ if (typeof h1Element.params.text === "object") {
87
+ for (const lang of Object.keys(h1Element.params.text)) {
88
+ h1Element.params.text[lang] += ` ${suffix[lang] || ""}`;
89
+ }
90
+ }
91
+ }
92
+ }
93
+ }
94
+
95
+
96
+ //⚙️ Process forms before rendering
97
+ pageTemplateCopy.sections.forEach(section => {
98
+ section.elements.forEach(element => {
99
+ if (element.element === "form") {
100
+ logger.debug("Processing form element for multipleThings item:", element, req);
101
+ // set form action
102
+ element.params.action = actionUrl;
103
+ // Set form method to POST
104
+ element.params.method = "POST";
105
+ // ➕ Add CSRF token
106
+ element.params.elements.push(govcyResources.csrfTokenInput(req.csrfToken()));
107
+ // 🔍 Find the first button with `prototypeNavigate`
108
+ const button = element.params.elements.find(subElement =>
109
+ // subElement.element === "button" && subElement.params.prototypeNavigate
110
+ subElement.element === "button"
111
+ );
112
+
113
+ // ⚙️ Modify the button if it exists
114
+ if (button) {
115
+ // Remove `prototypeNavigate`
116
+ if (button.params.prototypeNavigate) {
117
+ delete button.params.prototypeNavigate;
118
+ }
119
+ // Set `type` to "submit"
120
+ button.params.type = "submit";
121
+ }
122
+
123
+ // Handle form data
124
+ let theData = {};
125
+
126
+ //--------- Handle Validation Errors ---------
127
+ let validationErrors = null;
128
+
129
+ // Get all validation errors for this page (could be plain object or keyed map)
130
+ let validationErrorsAll = dataLayer.getPageValidationErrors(req.session, siteId, pageUrl);
131
+
132
+ if (validationErrorsAll) {
133
+ // Determine whether this is add/edit
134
+ const { index } = req.params;
135
+ const isAdd = req.originalUrl.includes("/multiple/add");
136
+ const key = isAdd ? "add" : (index !== undefined ? index : null);
137
+
138
+ if (key) {
139
+ // If not keyed yet, wrap them under this key
140
+ if (!validationErrorsAll[key]
141
+ && (validationErrorsAll.errors || validationErrorsAll.errorSummary)) {
142
+ validationErrorsAll = { [key]: validationErrorsAll };
143
+ }
144
+ validationErrors = validationErrorsAll[key] || null;
145
+ } else {
146
+ // Normal single-page case
147
+ validationErrors = validationErrorsAll;
148
+ }
149
+ }
150
+
151
+ // Populate form data
152
+ if (validationErrors) {
153
+ theData = validationErrors.formData || {};
154
+ } else {
155
+ theData = initialData || {};
156
+ }
157
+ //--------- End of Handle Validation Errors ---------
158
+
159
+
160
+ populateFormData(
161
+ element.params.elements,
162
+ theData,
163
+ validationErrors,
164
+ req.session,
165
+ siteId,
166
+ pageUrl,
167
+ req.globalLang,
168
+ null,
169
+ req.query.route,
170
+ mode,
171
+ index
172
+ );
173
+ // if there are validation errors, add an error summary
174
+ if (validationErrors?.errorSummary?.length > 0) {
175
+ element.params.elements.unshift(
176
+ govcyResources.errorSummary(validationErrors.errorSummary)
177
+ );
178
+ }
179
+
180
+ logger.debug("Processed multipleThings item form element:", element, req);
181
+ }
182
+ });
183
+ });
184
+
185
+ // Attach processed page
186
+ req.processedPage = {
187
+ pageData: {
188
+ "site": serviceCopy.site,
189
+ "pageData": {
190
+ "title": page.pageData.title,
191
+ "layout": page.pageData.layout,
192
+ "mainLayout": page.pageData.mainLayout
193
+ }
194
+ },
195
+ pageTemplate: pageTemplateCopy
196
+ };
197
+
198
+ logger.debug("Processed multipleThings item page:", req.processedPage, req);
199
+ next();
200
+ }
201
+
202
+ /**
203
+ * GET handler for add new item
204
+ */
205
+ export function govcyMultipleThingsAddHandler() {
206
+ return (req, res, next) => {
207
+ try {
208
+
209
+ const { siteId, pageUrl } = req.params;
210
+ const route = req.query?.route;
211
+ const actionUrl = `/${siteId}/${pageUrl}/multiple/add${route === "review" ? `?route=review` : ""}`;
212
+ // Use draft if it exists, otherwise seed an empty one
213
+ let draft = dataLayer.getMultipleDraft(req.session, siteId, pageUrl);
214
+ if (!draft) {
215
+ draft = {};
216
+ dataLayer.setMultipleDraft(req.session, siteId, pageUrl, draft);
217
+ }
218
+ multiplePageBuilder(req, res, next, draft, actionUrl, "add", null);
219
+ } catch (error) {
220
+ return next(error);
221
+ }
222
+ };
223
+ }
224
+
225
+ /**
226
+ * GET handler for edit existing item
227
+ */
228
+ export function govcyMultipleThingsEditHandler() {
229
+ return (req, res, next) => {
230
+ try {
231
+ const { siteId, pageUrl, index } = req.params;
232
+ const route = req.query?.route;
233
+
234
+ // Validate index
235
+ const idx = parseInt(index, 10);
236
+ let items = dataLayer.getPageData(req.session, siteId, pageUrl);
237
+ if (!Array.isArray(items)) items = [];
238
+
239
+ if (Number.isNaN(idx) || idx < 0 || idx >= items.length) {
240
+ return handleMiddlewareError(
241
+ `🚨 multipleThings edit index not found for ${siteId}/${pageUrl} (index=${index})`,
242
+ 404,
243
+ next
244
+ );
245
+ }
246
+
247
+ const initialData = items[idx];
248
+ const actionUrl = `/${siteId}/${pageUrl}/multiple/edit/${idx}${route === "review" ? `?route=review` : ""}`;
249
+ multiplePageBuilder(req, res, next, initialData, actionUrl, "edit", idx);
250
+
251
+ } catch (error) {
252
+ return next(error);
253
+ }
254
+ };
255
+ }
256
+ /**
257
+ *
258
+ * POST handler for adding a new item
259
+ */
260
+ export function govcyMultipleThingsAddPostHandler() {
261
+ return (req, res, next) => {
262
+ try {
263
+ const { siteId, pageUrl } = req.params;
264
+ const service = req.serviceData;
265
+ const page = getPageConfigData(service, pageUrl);
266
+
267
+ // 1. Check page conditions
268
+ const conditionResult = evaluatePageConditions(page, req.session, siteId, req);
269
+ if (conditionResult.result === false) {
270
+ return res.redirect(govcyResources.constructPageUrl(siteId, conditionResult.redirect, (req.query?.route === "review" ? "review" : "")));
271
+ }
272
+
273
+ // 2. Find form element
274
+ let formElement = null;
275
+ for (const section of page.pageTemplate.sections) {
276
+ formElement = section.elements.find(el => el.element === "form");
277
+ if (formElement) break;
278
+ }
279
+ if (!formElement) {
280
+ return handleMiddlewareError("🚨 Form definition not found.", 500, next);
281
+ }
282
+
283
+ // 3. Get form data
284
+ const formData = getFormData(formElement.params.elements, req.body, req.session, siteId, pageUrl);
285
+
286
+ // 4. Validate
287
+ const validationErrors = validateFormElements(formElement.params.elements, formData);
288
+ if (Object.keys(validationErrors).length > 0) {
289
+ // store validation errors under the "add" key
290
+ dataLayer.storePageValidationErrors(req.session, siteId, pageUrl, validationErrors, formData, "add");
291
+ return res.redirect(govcyResources.constructErrorSummaryUrl(req.originalUrl));
292
+ }
293
+
294
+ // 5. Commit new item into array
295
+ let items = dataLayer.getPageData(req.session, siteId, pageUrl);
296
+ if (!Array.isArray(items)) items = [];
297
+
298
+ const mtConfig = page.multipleThings;
299
+ // Check max limit
300
+ // Sanity check
301
+ if (!mtConfig || !mtConfig.max) {
302
+ return handleMiddlewareError("🚨 multipleThings.max not configured.", 500, next);
303
+ }
304
+
305
+ if (!mtConfig.listPage || !mtConfig.listPage.title) {
306
+ return handleMiddlewareError(`🚨 multipleThings.listPage.title is required for ${siteId}/${pageUrl}`, 404, next);
307
+ }
308
+
309
+ // 6. Enforce max limit
310
+ if (items.length >= mtConfig.max) {
311
+ // process message
312
+ // Deep copy page title (so we don’t mutate template)
313
+ let maxMsg = JSON.parse(JSON.stringify(govcyResources.staticResources.text.multipleThingsMaxMessage));
314
+ // Replace label placeholders on page title
315
+ for (const lang of Object.keys(maxMsg)) {
316
+ maxMsg[lang] = maxMsg[lang].replace("{{max}}", mtConfig.max);
317
+ }
318
+
319
+ dataLayer.storePageValidationErrors(req.session, siteId, pageUrl,
320
+ {
321
+ _global:
322
+ {
323
+ message: maxMsg,
324
+ pageUrl: govcyResources.constructPageUrl(siteId, pageUrl, (req.query?.route === "review" ? "review" : ""))
325
+ }
326
+ },
327
+ formData,
328
+ "add"
329
+ );
330
+ return res.redirect(govcyResources.constructErrorSummaryUrl(req.originalUrl));
331
+ }
332
+
333
+ // 7. Check dedupe
334
+ if (mtConfig.dedupe) {
335
+ const env = new nunjucks.Environment(null, { autoescape: false });
336
+ const newTitle = env.renderString(mtConfig.itemTitleTemplate, formData);
337
+ const duplicate = items.some(it => env.renderString(mtConfig.itemTitleTemplate, it) === newTitle);
338
+ if (duplicate) {
339
+ dataLayer.storePageValidationErrors(req.session, siteId, pageUrl,
340
+ {
341
+ _global:
342
+ {
343
+ message: govcyResources.staticResources.text.multipleThingsDedupeMessage
344
+ }
345
+ },
346
+ formData,
347
+ "add"
348
+ );
349
+ return res.redirect(govcyResources.constructErrorSummaryUrl(req.originalUrl));
350
+ }
351
+ }
352
+
353
+ // 8. Save item + clear draft
354
+ items.push(formData);
355
+ dataLayer.storePageData(req.session, siteId, pageUrl, items);
356
+ dataLayer.clearMultipleDraft(req.session, siteId, pageUrl);
357
+
358
+ // 9. Temp save
359
+ (async () => { try { await tempSaveIfConfigured(req.session, service, siteId); } catch (e) { } })();
360
+
361
+ // 10. Redirect back to the hub
362
+ return res.redirect(govcyResources.constructPageUrl(siteId, pageUrl, (req.query?.route === "review" ? "review" : "")));
363
+ } catch (error) { return next(error); }
364
+ };
365
+ }
366
+
367
+ /**
368
+ * POST handler for editing an existing item
369
+ */
370
+ export function govcyMultipleThingsEditPostHandler() {
371
+ return (req, res, next) => {
372
+ try {
373
+ const { siteId, pageUrl, index } = req.params;
374
+ const service = req.serviceData;
375
+ const page = getPageConfigData(service, pageUrl);
376
+
377
+ // 1. Check page conditions
378
+ const conditionResult = evaluatePageConditions(page, req.session, siteId, req);
379
+ if (conditionResult.result === false) {
380
+ return res.redirect(govcyResources.constructPageUrl(siteId, conditionResult.redirect, (req.query?.route === "review" ? "review" : "")));
381
+ }
382
+
383
+ // 2. Find form element
384
+ let formElement = null;
385
+ for (const section of page.pageTemplate.sections) {
386
+ formElement = section.elements.find(el => el.element === "form");
387
+ if (formElement) break;
388
+ }
389
+ if (!formElement) {
390
+ return handleMiddlewareError("🚨 Form definition not found.", 500, next);
391
+ }
392
+
393
+ // 3. Get form data
394
+ const formData = getFormData(formElement.params.elements, req.body, req.session, siteId, pageUrl, index);
395
+
396
+ // 4. Get current items array
397
+ let items = dataLayer.getPageData(req.session, siteId, pageUrl);
398
+ if (!Array.isArray(items)) items = [];
399
+
400
+ const idx = parseInt(index, 10);
401
+ if (Number.isNaN(idx) || idx < 0 || idx >= items.length) {
402
+ return handleMiddlewareError(
403
+ `🚨 multipleThings edit index not found for ${siteId}/${pageUrl} (index=${index})`,
404
+ 404,
405
+ next
406
+ );
407
+ }
408
+
409
+ // 5. Validate
410
+ const validationErrors = validateFormElements(formElement.params.elements, formData);
411
+ if (Object.keys(validationErrors).length > 0) {
412
+ dataLayer.storePageValidationErrors(req.session, siteId, pageUrl, validationErrors, formData, index);
413
+ return res.redirect(govcyResources.constructErrorSummaryUrl(req.originalUrl));
414
+ }
415
+
416
+ // 6. Dedupe check (skip current index)
417
+ const mtConfig = page.multipleThings;
418
+
419
+ // Sanity check
420
+ if (!mtConfig) {
421
+ return handleMiddlewareError("🚨 multipleThings not configured.", 500, next);
422
+ }
423
+
424
+ if (!mtConfig.listPage || !mtConfig.listPage.title) {
425
+ return handleMiddlewareError(`🚨 multipleThings.listPage.title is required for ${siteId}/${pageUrl}`, 404, next);
426
+ }
427
+
428
+ if (mtConfig?.dedupe) {
429
+ const env = new nunjucks.Environment(null, { autoescape: false });
430
+ const newTitle = env.renderString(mtConfig.itemTitleTemplate, formData);
431
+ const duplicate = items.some((it, i) =>
432
+ i !== idx && env.renderString(mtConfig.itemTitleTemplate, it) === newTitle
433
+ );
434
+ if (duplicate) {
435
+ dataLayer.storePageValidationErrors(req.session, siteId, pageUrl,
436
+ { _global: { message: govcyResources.staticResources.text.multipleThingsDedupeMessage } },
437
+ formData,
438
+ index
439
+ );
440
+ return res.redirect(govcyResources.constructErrorSummaryUrl(req.originalUrl));
441
+ }
442
+ }
443
+
444
+ // 7. Save back into array
445
+ items[idx] = formData;
446
+ dataLayer.storePageData(req.session, siteId, pageUrl, items);
447
+
448
+ // 8. Temp save
449
+ (async () => {
450
+ try { await tempSaveIfConfigured(req.session, service, siteId); }
451
+ catch (e) { /* already logged */ }
452
+ })();
453
+
454
+ // 9. Redirect back to the hub
455
+ return res.redirect(govcyResources.constructPageUrl(siteId, pageUrl, (req.query?.route === "review" ? "review" : "")));
456
+ } catch (error) {
457
+ return next(error);
458
+ }
459
+ };
460
+ }
@@ -4,6 +4,7 @@ import * as govcyResources from "../resources/govcyResources.mjs";
4
4
  import * as dataLayer from "../utils/govcyDataLayer.mjs";
5
5
  import { logger } from "../utils/govcyLogger.mjs";
6
6
  import { evaluatePageConditions } from "../utils/govcyExpressions.mjs";
7
+ import { govcyMultipleThingsHubHandler } from "./govcyMultipleThingsHubHandler.mjs";
7
8
  // import {flattenContext, evaluateExpressionWithFlattening, evaluatePageConditions } from "../utils/govcyExpressions.mjs";
8
9
 
9
10
  /**
@@ -38,11 +39,13 @@ export function govcyPageHandler() {
38
39
  if (result.result === false) {
39
40
  return res.redirect(`/${req.params.siteId}/${result.redirect}`);
40
41
  }
41
-
42
- //if user is logged in add the user nane section in the page template
43
- if (dataLayer.getUser(req.session)) {
44
- pageTemplateCopy.sections.push(govcyResources.userNameSection(dataLayer.getUser(req.session).name)); // Add user name section
42
+
43
+ // ----- MultipleThings hub handling
44
+ if (page.multipleThings) {
45
+ logger.debug(`Rendering multipleThings hub for pageUrl: ${pageUrl}`, req);
46
+ return govcyMultipleThingsHubHandler(req, res, next, page, serviceCopy);
45
47
  }
48
+
46
49
  //⚙️ Process forms before rendering
47
50
  pageTemplateCopy.sections.forEach(section => {
48
51
  section.elements.forEach(element => {
@@ -87,7 +90,16 @@ export function govcyPageHandler() {
87
90
  }
88
91
  //--------- End of Handle Validation Errors ---------
89
92
 
90
- populateFormData(element.params.elements, theData,validationErrors, req.session, siteId, pageUrl, req.globalLang, null, req.query.route);
93
+ populateFormData(
94
+ element.params.elements,
95
+ theData,
96
+ validationErrors,
97
+ req.session,
98
+ siteId,
99
+ pageUrl,
100
+ req.globalLang,
101
+ null,
102
+ req.query.route);
91
103
  // if there are validation errors, add an error summary
92
104
  if (validationErrors?.errorSummary?.length > 0) {
93
105
  element.params.elements.unshift(govcyResources.errorSummary(validationErrors.errorSummary));
@@ -1,5 +1,6 @@
1
1
  import { govcyFrontendRenderer } from "@gov-cy/govcy-frontend-renderer";
2
2
  import * as govcyResources from "../resources/govcyResources.mjs";
3
+ import * as dataLayer from "../utils/govcyDataLayer.mjs";
3
4
 
4
5
  /**
5
6
  * Middleware function to render pages using the GovCy Frontend Renderer.
@@ -18,6 +19,11 @@ export function renderGovcyPage() {
18
19
  const renderer = new govcyFrontendRenderer();
19
20
  const { processedPage } = req;
20
21
  processedPage.pageTemplate.sections.push(afterBody);
22
+
23
+ //if user is logged in add the user name section in the page template
24
+ if (dataLayer.getUser(req.session)) {
25
+ processedPage.pageTemplate.sections.push(govcyResources.userNameSection(dataLayer.getUser(req.session).name)); // Add user name section
26
+ }
21
27
  const html = renderer.renderFromJSON(processedPage.pageTemplate, processedPage.pageData);
22
28
  res.send(html);
23
29
  };
@@ -1,8 +1,9 @@
1
1
  import * as govcyResources from "../resources/govcyResources.mjs";
2
2
  import * as dataLayer from "../utils/govcyDataLayer.mjs";
3
3
  import { logger } from "../utils/govcyLogger.mjs";
4
- import {preparePrintFriendlyData , generateReviewSummary } from "../utils/govcySubmitData.mjs";
4
+ import { preparePrintFriendlyData, generateReviewSummary } from "../utils/govcySubmitData.mjs";
5
5
  import { whatsIsMyEnvironment } from '../utils/govcyEnvVariables.mjs';
6
+ import { buildMultipleThingsValidationSummary } from "../utils/govcyMultipleThingsValidation.mjs";
6
7
 
7
8
 
8
9
  /**
@@ -13,23 +14,23 @@ export function govcyReviewPageHandler() {
13
14
  return (req, res, next) => {
14
15
  try {
15
16
  const { siteId } = req.params;
16
-
17
+
17
18
  // Create a deep copy of the service to avoid modifying the original
18
19
  let serviceCopy = req.serviceData;
19
-
20
+
20
21
  // Deep copy renderer pageData from
21
22
  let pageData = JSON.parse(JSON.stringify(govcyResources.staticResources.rendererPageData));
22
-
23
+
23
24
  // Handle isTesting
24
25
  pageData.site.isTesting = (whatsIsMyEnvironment() === "staging");
25
-
26
+
26
27
  // Base page template structure
27
28
  let pageTemplate = {
28
29
  sections: [
29
- {
30
- name: "beforeMain",
31
- elements: [govcyResources.staticResources.elements.backLink]
32
- }
30
+ // {
31
+ // name: "beforeMain",
32
+ // elements: [govcyResources.staticResources.elements.backLink]
33
+ // }
33
34
  ]
34
35
  };
35
36
  // Construct page title
@@ -40,12 +41,12 @@ export function govcyReviewPageHandler() {
40
41
  // if serviceCopy has site.reviewPageHeader use it otherwise use the static resource. it should test if serviceCopy.site.reviewPageHeader[req.globalLang] exists
41
42
  text: (
42
43
  serviceCopy?.site?.reviewPageHeader?.[req.globalLang]
43
- ? serviceCopy.site.reviewPageHeader
44
- : govcyResources.staticResources.text.checkYourAnswersTitle
44
+ ? serviceCopy.site.reviewPageHeader
45
+ : govcyResources.staticResources.text.checkYourAnswersTitle
45
46
  )
46
47
  }
47
48
  };
48
-
49
+
49
50
  // Construct submit button
50
51
  const submitButton = {
51
52
  element: "form",
@@ -66,40 +67,65 @@ export function govcyReviewPageHandler() {
66
67
  }
67
68
  // Generate the summary list using the utility function
68
69
  let printFriendlyData = preparePrintFriendlyData(req, siteId, serviceCopy);
69
- let summaryList = generateReviewSummary(printFriendlyData,req, siteId);
70
-
70
+ let summaryList = generateReviewSummary(printFriendlyData, req, siteId);
71
+
72
+ let mainElements = [];
71
73
  //--------- Handle Validation Errors ---------
72
74
  // Check if validation errors exist in the session
73
75
  const validationErrors = dataLayer.getSiteSubmissionErrors(req.session, siteId);
74
- let mainElements = [];
75
- if (validationErrors ) {
76
- for (const error in validationErrors.errors) {
77
- validationErrors.errorSummary.push({
78
- link: govcyResources.constructPageUrl(siteId, validationErrors.errors[error].pageUrl, "review"), //`/${siteId}/${error.pageUrl}`,
79
- text: validationErrors.errors[error].message
80
- });
76
+ let summaryItems = [];
77
+
78
+ if (validationErrors && validationErrors.errors) {
79
+ for (const [pageUrl, err] of Object.entries(validationErrors.errors)) {
80
+ // Handle multipleThings hub errors
81
+ if (err.type === "multipleThings") {
82
+ const mtErrors = err.hub?.errors || {};
83
+ // Build summary items for multipleThings errors
84
+ summaryItems.push(...buildMultipleThingsValidationSummary( serviceCopy, mtErrors, siteId, pageUrl, req, "review", false));
85
+ } else {
86
+ // Normal pages: loop through field errors
87
+ for (const [fieldKey, fieldErr] of Object.entries(err)) {
88
+ // Skip type field
89
+ if (fieldKey === "type") continue;
90
+ // Push each field error to summary items
91
+ summaryItems.push({
92
+ link: govcyResources.constructPageUrl(siteId, fieldErr.pageUrl, "review"),
93
+ text: fieldErr.message
94
+ });
95
+ }
96
+ }
81
97
  }
82
- mainElements.push(govcyResources.errorSummary(validationErrors.errorSummary));
83
- }
98
+ }
99
+
100
+ if (summaryItems.length > 0) {
101
+ mainElements.unshift(govcyResources.errorSummary(summaryItems));
102
+ }
103
+
104
+ // const validationErrors = dataLayer.getSiteSubmissionErrors(req.session, siteId);
105
+ // let mainElements = [];
106
+ // if (validationErrors ) {
107
+ // for (const error in validationErrors.errors) {
108
+ // validationErrors.errorSummary.push({
109
+ // link: govcyResources.constructPageUrl(siteId, validationErrors.errors[error].pageUrl, "review"), //`/${siteId}/${error.pageUrl}`,
110
+ // text: validationErrors.errors[error].message
111
+ // });
112
+ // }
113
+ // mainElements.push(govcyResources.errorSummary(validationErrors.errorSummary));
114
+ // }
84
115
  //--------- End Handle Validation Errors ---------
85
116
 
86
117
  // Add elements to the main section, the H1, summary list, the submit button and the JS
87
- mainElements.push(pageH1,
88
- summaryList,
118
+ mainElements.push(pageH1,
119
+ summaryList,
89
120
  submitButton
90
121
  );
91
122
  // Append generated summary list to the page template
92
123
  pageTemplate.sections.push({ name: "main", elements: mainElements });
93
-
94
- //if user is logged in add he user bane section in the page template
95
- if (dataLayer.getUser(req.session)) {
96
- pageTemplate.sections.push(govcyResources.userNameSection(dataLayer.getUser(req.session).name)); // Add user name section
97
- }
98
-
124
+
99
125
  //prepare pageData
100
126
  pageData.site = serviceCopy.site;
101
127
  pageData.pageData.title = govcyResources.staticResources.text.checkYourAnswersTitle;
102
-
128
+
103
129
  // Attach processed page data to the request
104
130
  req.processedPage = {
105
131
  pageData: pageData,