@gov-cy/govcy-express-services 1.8.2 → 1.9.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.
@@ -0,0 +1,560 @@
1
+ import { computeTaskListStatus } from "../utils/govcyTaskList.mjs";
2
+ import * as govcyResources from "../resources/govcyResources.mjs";
3
+ import * as dataLayer from "../utils/govcyDataLayer.mjs";
4
+ import { logger } from "../utils/govcyLogger.mjs";
5
+ import { handleMiddlewareError } from "../utils/govcyUtils.mjs";
6
+
7
+ // The CSS classes for each task status
8
+ const STATUS_TAG_CLASSES = {
9
+ NOT_STARTED: "govcy-tag-gray",
10
+ IN_PROGRESS: "govcy-tag-cyan",
11
+ COMPLETED: "",
12
+ SKIPPED: "govcy-tag-gray"
13
+ };
14
+
15
+ /**
16
+ * Task list GET middleware – mirrors the bespoke Update My Details handler, but
17
+ * instead of rebuilding the UMD template it composes a light-weight renderer
18
+ * template that shows: optional top elements, a localized overall status
19
+ * summary, the GOV.CY taskList element, and a continue button.
20
+ *
21
+ * @param {object} req Express request object
22
+ * @param {object} res Express response object
23
+ * @param {Function} next Express next callback
24
+ * @param {object} page Task list page configuration
25
+ * @param {object} service Service data (req.serviceData)
26
+ */
27
+ export function govcyTaskListHandler(req, res, next, page, service) {
28
+ try {
29
+ const { siteId, pageUrl } = req.params;
30
+ const lang = req.globalLang || service?.site?.lang || "el";
31
+ // Handle the route
32
+ const route = req.query?.route === "review" ? "review" : "";
33
+
34
+ const taskListConfig = page?.taskList || {};
35
+ const taskPages = Array.isArray(taskListConfig.taskPages) ? taskListConfig.taskPages : [];
36
+ const showSkippedTasks = taskListConfig.showSkippedTasks === true;
37
+
38
+ // Compute task statuses and overall summary using the same logic as review
39
+ const summary = computeTaskListStatus(req, siteId, service, taskPages);
40
+
41
+ // Start with a simple form scaffold and progressively append renderer elements
42
+ const pageTemplate = buildBaseTemplate(req, siteId, page, route);
43
+ const formElements = pageTemplate.sections[0].elements[0].params.elements;
44
+
45
+ // Surface any POST validation errors that were stored in the session
46
+ const validationErrors = dataLayer.getPageValidationErrors(req.session, siteId, pageUrl);
47
+ if (validationErrors?.errorSummary?.length > 0) {
48
+ formElements.push(
49
+ govcyResources.errorSummary(validationErrors.errorSummary, {
50
+ body: validationErrors.body,
51
+ linkToContinue: validationErrors.linkToContinue
52
+ })
53
+ );
54
+ }
55
+
56
+ // Allow services to prepend arbitrary content before the status table
57
+ if (Array.isArray(taskListConfig.topElements) && taskListConfig.topElements.length > 0) {
58
+ formElements.push(...deepClone(taskListConfig.topElements));
59
+ }
60
+
61
+ // High-level status summary (localized tag + completion counter)
62
+ formElements.push(buildOverallStatusSection(summary, showSkippedTasks));
63
+
64
+ // Converts the raw computeTaskListStatus output into renderer taskList rows
65
+ const taskItems = buildTaskListItems({
66
+ tasks: summary.tasks,
67
+ siteId,
68
+ lang,
69
+ route,
70
+ showSkippedTasks
71
+ });
72
+
73
+ if (taskItems.length > 0) {
74
+ // Render the GOV.CY task list component with per-row tags
75
+ formElements.push({
76
+ element: "taskList",
77
+ params: {
78
+ id: `${pageUrl}-task-list`,
79
+ lang,
80
+ items: taskItems
81
+ }
82
+ });
83
+ } else {
84
+ // Defensive fallback if taskPages is empty/misconfigured
85
+ formElements.push({
86
+ element: "inset",
87
+ params: {
88
+ text: govcyResources.staticResources.text.taskListEmptyState
89
+ }
90
+ });
91
+ }
92
+
93
+ // Continue button keeps navigation consistent with standard forms
94
+ formElements.push(buildContinueButton(taskListConfig));
95
+
96
+ if (taskListConfig.hasBackLink) {
97
+ // Optional backlink replicates the pattern used in UMD/multipleThings
98
+ pageTemplate.sections.unshift({
99
+ name: "beforeMain",
100
+ elements: [{ element: "backLink", params: {} }]
101
+ });
102
+ }
103
+
104
+ req.processedPage = {
105
+ pageData: {
106
+ site: service?.site,
107
+ pageData: {
108
+ title: page?.pageData?.title,
109
+ layout: page?.pageData?.layout || "layouts/govcyBase.njk",
110
+ mainLayout: page?.pageData?.mainLayout || "two-third"
111
+ }
112
+ },
113
+ pageTemplate
114
+ };
115
+
116
+ return next();
117
+ } catch (error) {
118
+ logger.error("Failed to render task list page", {
119
+ siteId: req.params?.siteId,
120
+ pageUrl: req.params?.pageUrl,
121
+ message: error?.message
122
+ });
123
+ return next(error);
124
+ }
125
+ }
126
+
127
+ /**
128
+ * Returns a minimal page template with a single <form> element. The caller will
129
+ * append the sections/elements for errors, headers, and task lists.
130
+ *
131
+ * @param {object} req Express request
132
+ * @param {string} siteId Site identifier
133
+ * @param {object} page Page configuration
134
+ * @param {string} route Optional review route flag
135
+ * @returns {object} Renderer page template scaffold
136
+ */
137
+ function buildBaseTemplate(req, siteId, page, route) {
138
+ return {
139
+ sections: [
140
+ {
141
+ name: "main",
142
+ elements: [
143
+ {
144
+ element: "form",
145
+ params: {
146
+ action: govcyResources.constructPageUrl(siteId, page?.pageData?.url, route),
147
+ method: "POST",
148
+ elements: [govcyResources.csrfTokenInput(req.csrfToken())]
149
+ }
150
+ }
151
+ ]
152
+ }
153
+ ]
154
+ };
155
+ }
156
+
157
+ /**
158
+ * Creates the HTML block that sits above the task list, showing the localized
159
+ * overall status and how many tasks are complete. The GOV.CY renderer does not
160
+ * have this element pre-built yet, so we emit it via htmlElement.
161
+ *
162
+ * @param {{status:string,tasks:Array}} summary Result from computeTaskListStatus
163
+ * @param {boolean} showSkippedTasks Whether skipped rows are visible to users
164
+ * @returns {object} htmlElement renderer object
165
+ */
166
+ function buildOverallStatusSection(summary, showSkippedTasks) {
167
+ const statusKey = summary?.status || "NOT_STARTED";
168
+ // get localized status
169
+ const localizedStatus = govcyResources.staticResources.text.taskListStatus[statusKey] ||
170
+ govcyResources.staticResources.text.taskListStatus.NOT_STARTED;
171
+
172
+ // Remove SKIPPED rows when the service opted not to show them
173
+ let displayedTasks = filterDisplayedTasks(summary?.tasks || [], showSkippedTasks);
174
+ // Count how many tasks are COMPLETED
175
+ let completedCount = displayedTasks.filter(task => task.status === "COMPLETED").length;
176
+ // Count total tasks (after filtering out SKIPPED if they are hidden)
177
+ let totalCount = displayedTasks.filter(task => task.status !== "SKIPPED").length;
178
+
179
+ // Build completion summary text with placeholders replaced, e.g. "You have completed 2 of 5 tasks"
180
+ const summaryTemplate = govcyResources.staticResources.text.taskListCompletionSummary;
181
+ const summaryText = replacePlaceholders(
182
+ summaryTemplate,
183
+ { completed: completedCount, total: totalCount }
184
+ );
185
+
186
+ // Get localized overall status label
187
+ const overallLabel = govcyResources.staticResources.text.taskListOverallLabel;
188
+
189
+ return {
190
+ element: "htmlElement",
191
+ params: {
192
+ text: {
193
+ en: buildOverallHtml(overallLabel.en, localizedStatus.en, summaryText.en),
194
+ el: buildOverallHtml(overallLabel.el, localizedStatus.el, summaryText.el),
195
+ tr: buildOverallHtml(overallLabel.tr, localizedStatus.tr, summaryText.tr)
196
+ }
197
+ }
198
+ };
199
+ }
200
+
201
+ function buildOverallHtml(label, statusText, summaryLine) {
202
+ // return `<section class="govcy-mb-4">
203
+ // <p class="govcy-fs-6 govcy-fw-400 govcy-text-muted">${label}</p>
204
+ // <p class="govcy-fs-5 govcy-fw-700">${statusText}</p>
205
+ // <p>${summaryLine}</p>
206
+ // </section>`;
207
+ return `<section class="govcy-mb-4">
208
+ <p>${summaryLine}</p>
209
+ </section>`;
210
+ }
211
+
212
+ /**
213
+ * Converts the raw computeTaskListStatus output into renderer taskList rows,
214
+ * adding localized status tags, per-row links, and optional descriptions.
215
+ *
216
+ * @param {object} opts
217
+ * @param {Array} opts.tasks Array of per-page status payloads
218
+ * @param {string} opts.siteId Site identifier
219
+ * @param {string} opts.lang Current language (used for localized URLs)
220
+ * @param {string} opts.route Route query flag, e.g. "review"
221
+ * @param {boolean} opts.showSkippedTasks Whether to include SKIPPED rows
222
+ * @returns {Array} GOV.CY renderer task list items
223
+ */
224
+ function buildTaskListItems({ tasks = [], siteId, lang, route, showSkippedTasks }) {
225
+ const items = [];
226
+ for (const task of tasks) {
227
+ if (!task) continue;
228
+ if (task.status === "SKIPPED" && !showSkippedTasks) {
229
+ // Hidden rows still factor into overall completion but are not shown
230
+ continue;
231
+ }
232
+
233
+ const statusKey = normalizeStatus(task.status);
234
+ const statusText = statusKey === "SKIPPED"
235
+ ? govcyResources.staticResources.text.taskListNotApplicable
236
+ : govcyResources.staticResources.text.taskListStatus[statusKey] || govcyResources.staticResources.text.taskListStatus.NOT_STARTED;
237
+
238
+ const title = task.title || govcyResources.staticResources.text.untitled;
239
+ const href = statusKey === "SKIPPED" ? null : buildTaskLink(siteId, task.pageUrl, route);
240
+
241
+ items.push({
242
+ id: `${sanitizeId(task.pageUrl)}-task`,
243
+ task: {
244
+ text: title,
245
+ ...(href ? { link: href } : {})
246
+ },
247
+ description: task?.description || null, // Optional copy from service config
248
+ status: {
249
+ text: statusText,
250
+ classes: STATUS_TAG_CLASSES[statusKey]
251
+ }
252
+ });
253
+ }
254
+ return items;
255
+ }
256
+
257
+ /**
258
+ * Builds the primary button element. Services can override the label via
259
+ * taskList.continueButtonText; otherwise the standard Continue copy is used.
260
+ *
261
+ * @param {object} taskListConfig Task list config block
262
+ * @returns {object} button renderer element
263
+ */
264
+ function buildContinueButton(taskListConfig) {
265
+ const configuredText = taskListConfig?.continueButtonText;
266
+ let textObject = govcyResources.staticResources.text.continue;
267
+ if (configuredText) {
268
+ if (typeof configuredText === "string") {
269
+ textObject = {
270
+ el: configuredText,
271
+ en: configuredText,
272
+ tr: configuredText
273
+ };
274
+ } else if (typeof configuredText === "object") {
275
+ textObject = { ...textObject, ...configuredText };
276
+ }
277
+ }
278
+
279
+ return {
280
+ element: "button",
281
+ params: {
282
+ variant: "primary",
283
+ type: "submit",
284
+ text: textObject
285
+ }
286
+ };
287
+ }
288
+
289
+ /**
290
+ * Removes SKIPPED rows when the service opted not to show them. This ensures
291
+ * the completion summary only counts the rows the user can see.
292
+ *
293
+ * @param {Array} tasks Raw tasks array
294
+ * @param {boolean} showSkippedTasks Whether to include skipped rows
295
+ * @returns {Array} Filtered tasks
296
+ */
297
+ function filterDisplayedTasks(tasks, showSkippedTasks) {
298
+ if (showSkippedTasks) {
299
+ return tasks;
300
+ }
301
+ return tasks.filter(task => task.status !== "SKIPPED");
302
+ }
303
+
304
+ /**
305
+ * Replaces {{completed}} and {{total}} placeholders while preserving the
306
+ * multilingual structure of the static resource definition.
307
+ *
308
+ * @param {object} templateObj Multilingual template object
309
+ * @param {{completed:number,total:number}} values Replacement values
310
+ * @returns {object} Multilingual object with replacements applied
311
+ */
312
+ function replacePlaceholders(templateObj, values) {
313
+ const build = (text) => {
314
+ if (typeof text !== "string") return "";
315
+ return text
316
+ .replace("{{completed}}", values.completed)
317
+ .replace("{{total}}", values.total);
318
+ };
319
+ return {
320
+ en: build(templateObj?.en || templateObj?.el || templateObj?.tr || ""),
321
+ el: build(templateObj?.el || templateObj?.en || templateObj?.tr || ""),
322
+ tr: build(templateObj?.tr || templateObj?.en || templateObj?.el || "")
323
+ };
324
+ }
325
+
326
+ /**
327
+ * Generates the hyperlink for a task row. Custom URLs (starting with /) are
328
+ * preserved; service-relative URLs go through constructPageUrl so language and
329
+ * review routes behave like normal navigation.
330
+ *
331
+ * @param {string} siteId Site identifier
332
+ * @param {string} pageUrl Page URL from the task config
333
+ * @param {string} route Optional route query
334
+ * @returns {string|null} Resolved href or null when no link should be shown
335
+ */
336
+ function buildTaskLink(siteId, pageUrl, route) {
337
+ if (!pageUrl) return null;
338
+ if (pageUrl.startsWith("/")) {
339
+ const hasQuery = pageUrl.includes("?");
340
+ if (!route) return pageUrl;
341
+ return `${pageUrl}${hasQuery ? "&" : "?"}route=${route}`;
342
+ }
343
+ return govcyResources.constructPageUrl(siteId, pageUrl, route);
344
+ }
345
+
346
+ /**
347
+ * Normalizes a pageUrl into a DOM-friendly ID to satisfy renderer requirements.
348
+ *
349
+ * @param {string} pageUrl Page URL
350
+ * @returns {string} Sanitized ID string
351
+ */
352
+ function sanitizeId(pageUrl = "") {
353
+ return pageUrl
354
+ .replace(/^\//, "")
355
+ .replace(/[^a-zA-Z0-9-_]/g, "-");
356
+ }
357
+
358
+ /**
359
+ * Normalizes status strings so renderer logic can rely on a constrained set of
360
+ * values. Unknown values revert to NOT_STARTED for safety.
361
+ *
362
+ * @param {string} statusKey Raw status key
363
+ * @returns {string} Normalized status constant
364
+ */
365
+ function normalizeStatus(statusKey = "") {
366
+ const upper = (statusKey || "").toUpperCase();
367
+ if (["NOT_STARTED", "IN_PROGRESS", "COMPLETED", "SKIPPED"].includes(upper)) {
368
+ return upper;
369
+ }
370
+ return "NOT_STARTED";
371
+ }
372
+
373
+ /**
374
+ * Helper for cloning config snippets before injecting them in the template.
375
+ * Using JSON stringify/parse is fine here because the config only contains
376
+ * simple data structures supported by the renderer schema.
377
+ *
378
+ * @param {*} value Serializable value
379
+ * @returns {*} Deep copy of the value
380
+ */
381
+ function deepClone(value) {
382
+ return JSON.parse(JSON.stringify(value));
383
+ }
384
+
385
+ /**
386
+ * Handles POST submissions for task-list pages. These pages do not carry form
387
+ * data; instead we recompute each task's status and decide whether the user can
388
+ * continue. When outstanding tasks exist we persist a tailored error summary so
389
+ * the renderer can surface actionable guidance.
390
+ *
391
+ * @param {object} req Express request
392
+ * @param {object} res Express response
393
+ * @param {Function} next Express next callback
394
+ * @param {object} ctx Convenience bundle with page, service, siteId, pageUrl
395
+ * @returns {object|void}
396
+ */
397
+ export function handleTaskListPost(req, res, next, { page, service, siteId, pageUrl }) {
398
+ // Task list pages do not have form data, but we still need to validate the current state of the world to see if they can continue.
399
+ const taskPages = Array.isArray(page?.taskList?.taskPages) ? page.taskList.taskPages : [];
400
+ const summary = computeTaskListStatus(req, siteId, service, taskPages);
401
+ const nextPageHref = resolveTaskListNextPage(page, siteId, req);
402
+
403
+ if (summary.status === "COMPLETED") {
404
+ if (!nextPageHref) {
405
+ return handleMiddlewareError("Task list page missing nextPage destination", 500, next);
406
+ }
407
+ return res.redirect(nextPageHref);
408
+ }
409
+
410
+ // Build one error-summary row per incomplete task to highlight next steps.
411
+ const summaryItems = buildTaskListErrorSummary(
412
+ summary.tasks,
413
+ siteId,
414
+ typeof req.query.route === "string" ? req.query.route : undefined
415
+ );
416
+ summaryItems.unshift(govcyResources.staticResources.text.taskListCompleteAll);
417
+
418
+ const allowContinue = Boolean(page?.taskList?.linkToContinue) &&
419
+ nextPageHref &&
420
+ req.query.route !== "review";
421
+
422
+ const options = {};
423
+ if (allowContinue) {
424
+ options.body = govcyResources.staticResources.text.taskListAllowContinueBody;
425
+ options.linkToContinue = {
426
+ text: govcyResources.staticResources.text.taskListContinueLink,
427
+ visuallyHiddenText: govcyResources.staticResources.text.taskListContinueHiddenText,
428
+ link: nextPageHref
429
+ };
430
+ }
431
+
432
+ storeTaskListValidationSummary(req.session, siteId, pageUrl, summaryItems, options);
433
+ return res.redirect(govcyResources.constructErrorSummaryUrl(req.originalUrl));
434
+ }
435
+
436
+ /**
437
+ * Resolves the destination URL for a task-list continue action. Mirrors the
438
+ * logic used for regular pages (respect review route overrides).
439
+ *
440
+ * @param {object} page Current page configuration
441
+ * @param {string} siteId Service identifier
442
+ * @param {object} req Express request
443
+ * @returns {string|null}
444
+ */
445
+ function resolveTaskListNextPage(page, siteId, req) {
446
+ if (req.query.route === "review") {
447
+ return govcyResources.constructPageUrl(siteId, "review");
448
+ }
449
+ const nextPage = page?.pageData?.nextPage;
450
+ if (!nextPage) return null;
451
+ return govcyResources.constructPageUrl(siteId, nextPage);
452
+ }
453
+
454
+ /**
455
+ * Creates an error-summary list for every task that still needs attention.
456
+ *
457
+ * @param {Array} tasks Task descriptor array from computeTaskListStatus
458
+ * @param {string} siteId Service identifier for building hrefs
459
+ * @param {string} [route] Optional route query (e.g. \"review\") to preserve context
460
+ * @returns {Array<{text:string, link?:string}>}
461
+ */
462
+ function buildTaskListErrorSummary(tasks = [], siteId, route) {
463
+ return tasks
464
+ .filter(task => task && task.status !== "COMPLETED" && task.status !== "SKIPPED")
465
+ .map(task => {
466
+ const item = {
467
+ text: buildTaskListErrorText(task.title)
468
+ };
469
+ if (task.pageUrl) {
470
+ item.link = govcyResources.constructPageUrl(siteId, task.pageUrl, route);
471
+ }
472
+ return item;
473
+ });
474
+ }
475
+
476
+ /**
477
+ * Produces a multilingual message like \"Complete the section {Title}\" for each task.
478
+ *
479
+ * @param {object} title Multilingual task title object
480
+ * @returns {object} Multilingual error summary text
481
+ */
482
+ function buildTaskListErrorText(title) {
483
+ const normalizedTitle = hasLocalizedContent(title)
484
+ ? title
485
+ : govcyResources.staticResources.text.untitled;
486
+ return combineLocalizedStrings(
487
+ govcyResources.staticResources.text.task?.title,
488
+ normalizedTitle
489
+ );
490
+ }
491
+
492
+ /**
493
+ * Concatenates two multilingual objects (prefix + value) while preserving fallbacks.
494
+ *
495
+ * @param {object|string} prefix Multilingual/string prefix
496
+ * @param {object|string} value Multilingual/string value
497
+ * @returns {object} Combined multilingual object
498
+ */
499
+ function combineLocalizedStrings(prefix, value) {
500
+ const languages = new Set(["el", "en", "tr"]);
501
+ if (hasLocalizedContent(prefix)) {
502
+ Object.keys(prefix).forEach(lang => languages.add(lang));
503
+ }
504
+ if (hasLocalizedContent(value)) {
505
+ Object.keys(value).forEach(lang => languages.add(lang));
506
+ }
507
+
508
+ const result = {};
509
+ languages.forEach(lang => {
510
+ const prefixText = resolveLocalizedText(prefix, lang);
511
+ const valueText = resolveLocalizedText(value, lang);
512
+ const combined = `${prefixText} ${valueText}`.trim();
513
+ result[lang] = combined || valueText || prefixText;
514
+ });
515
+ return result;
516
+ }
517
+
518
+ /**
519
+ * Returns true when a value looks like a multilingual object with keys.
520
+ *
521
+ * @param {any} value Potential multilingual object
522
+ * @returns {boolean}
523
+ */
524
+ function hasLocalizedContent(value) {
525
+ return value && typeof value === "object" && Object.keys(value).length > 0;
526
+ }
527
+
528
+ /**
529
+ * Safely resolves text for a single language, falling back to common locales.
530
+ *
531
+ * @param {object|string} source Multilingual/string source
532
+ * @param {string} lang Desired language key
533
+ * @returns {string}
534
+ */
535
+ function resolveLocalizedText(source, lang) {
536
+ if (!source) return "";
537
+ if (typeof source === "string") return source;
538
+ return source[lang] ?? source.el ?? source.en ?? source.tr ?? "";
539
+ }
540
+
541
+ /**
542
+ * Persists the synthesized error summary back in the session so the renderer
543
+ * can present GOV.CY error summary content without having to understand task
544
+ * logic.
545
+ *
546
+ * @param {object} store Session object (req.session)
547
+ * @param {string} siteId Service identifier
548
+ * @param {string} pageUrl Page identifier
549
+ * @param {Array} summaryItems Error summary entries
550
+ * @param {object} options Optional body / linkToContinue overrides
551
+ */
552
+ function storeTaskListValidationSummary(store, siteId, pageUrl, summaryItems, options = {}) {
553
+ dataLayer.storePageValidationErrors(store, siteId, pageUrl, {}, null);
554
+ const container = store?.siteData?.[siteId]?.inputData?.[pageUrl]?.validationErrors;
555
+ if (!container) return;
556
+ // Attach extra renderer-friendly metadata when provided.
557
+ container.errorSummary = summaryItems;
558
+ if (options.body) container.body = options.body;
559
+ if (options.linkToContinue) container.linkToContinue = options.linkToContinue;
560
+ }
@@ -226,10 +226,84 @@ export const staticResources = {
226
226
  en: "Yes, I want to delete this entry",
227
227
  tr: "Yes, I want to delete this entry"
228
228
  },
229
- multipleThingsDeleteNoOption: {
230
- el: "Όχι, δεν θέλω να διαγράψω την καταχώριση",
231
- en: "No, I don't want to delete this entry",
232
- tr: "No, I don't want to delete this entry"
229
+ multipleThingsDeleteNoOption: {
230
+ el: "Όχι, δεν θέλω να διαγράψω την καταχώριση",
231
+ en: "No, I don't want to delete this entry",
232
+ tr: "No, I don't want to delete this entry"
233
+ },
234
+ task: {
235
+ title: {
236
+ el: "Ολοκληρώστε την ενότητα",
237
+ en: "Complete the section",
238
+ tr: "Bölümü tamamlayın"
239
+ }
240
+ },
241
+ taskListStatus: {
242
+ NOT_STARTED: {
243
+ el: "Δεν ξεκίνησε",
244
+ en: "Not started",
245
+ tr: "Başlamadı"
246
+ },
247
+ IN_PROGRESS: {
248
+ el: "Σε εξέλιξη",
249
+ en: "In progress",
250
+ tr: "Devam ediyor"
251
+ },
252
+ COMPLETED: {
253
+ el: "Ολοκληρώθηκε",
254
+ en: "Completed",
255
+ tr: "Tamamlandı"
256
+ }
257
+ },
258
+ taskListCompleteAll: {
259
+ el: "Ολοκληρώστε όλες τις ενότητες πριν συνεχίσετε.",
260
+ en: "Complete all sections before continuing.",
261
+ tr: "Devam etmeden önce tüm bölümleri tamamlayın."
262
+ },
263
+ taskListCompleteTheSection: {
264
+ el: "Ολοκληρώστε την ενότητα ",
265
+ en: "Complete the section",
266
+ tr: "Bölümü tamamlayın "
267
+ },
268
+ taskListAllowContinueBody: {
269
+ el: "Μπορείτε να συνεχίσετε τώρα, αλλά θα πρέπει να επιστρέψετε και να ολοκληρώσετε τις υπόλοιπες ενότητες.",
270
+ en: "You can continue now, but you must return and finish the remaining sections.",
271
+ tr: "Şimdi devam edebilirsiniz ancak kalan bölümleri tamamlamak için geri dönmelisiniz."
272
+ },
273
+ taskListContinueLink: {
274
+ el: "Συνέχεια χωρίς να ολοκληρωθούν όλες οι ενότητες",
275
+ en: "Continue without completing all sections",
276
+ tr: "Tüm bölümleri tamamlamadan devam et"
277
+ },
278
+ taskListContinueHiddenText: {
279
+ el: "Συνέχεια παρότι δεν ολοκληρώθηκαν όλες οι ενότητες",
280
+ en: "Continue even though not all sections are complete",
281
+ tr: "Tüm bölümler tamamlanmamış olsa da devam et"
282
+ },
283
+ taskListNotApplicable: {
284
+ el: "Δεν εφαρμόζεται",
285
+ en: "Not applicable",
286
+ tr: "Geçerli değil"
287
+ },
288
+ taskListOverallLabel: {
289
+ el: "Συνολική κατάσταση",
290
+ en: "Overall status",
291
+ tr: "Genel durum"
292
+ },
293
+ taskListCompletionSummary: {
294
+ el: "Έχετε ολοκληρώσει <strong>{{completed}}</strong> από <strong>{{total}}</strong> ενότητες.",
295
+ en: "You've completed <strong>{{completed}}</strong> of <strong>{{total}}</strong> sections.",
296
+ tr: "<strong>{{total}}</strong> bölümün <strong>{{completed}}</strong> tanesini tamamladınız."
297
+ },
298
+ taskListErrorCompleteAll: {
299
+ el: "Πρέπει να ολοκληρώσετε όλες τις ενότητες πριν συνεχίσετε.",
300
+ en: "You must complete all sections before continuing.",
301
+ tr: "Devam etmeden önce tüm bölümleri tamamlamalısınız."
302
+ },
303
+ taskListEmptyState: {
304
+ el: "Δεν υπάρχουν ενότητες για εμφάνιση.",
305
+ en: "There are no sections to display.",
306
+ tr: "Gösterilecek bölüm yok."
233
307
  },
234
308
  updateMyDetailsTitle: {
235
309
  el: "Τα στοιχεία σας",
@@ -972,13 +1046,23 @@ export function constructPageUrl(siteId, pageUrl, route) {
972
1046
  * @param {array} errors The array of errors
973
1047
  * @returns The error summary element
974
1048
  */
975
- export function errorSummary(errors) {
1049
+ export function errorSummary(errors, options = {}) {
1050
+ const params = {
1051
+ id: "errorSummary",
1052
+ errors: errors
1053
+ };
1054
+ if (options.body) {
1055
+ params.body = options.body;
1056
+ }
1057
+ if (options.linkToContinue) {
1058
+ params.linkToContinue = options.linkToContinue;
1059
+ }
1060
+ if (options.header) {
1061
+ params.header = options.header;
1062
+ }
976
1063
  return {
977
1064
  element: "errorSummary",
978
- params: {
979
- id: "errorSummary",
980
- errors: errors
981
- }
1065
+ params
982
1066
  };
983
1067
  }
984
1068
 
@@ -1216,4 +1300,4 @@ export function getMultipleThingsLink(linkType, siteId, pageUrl, lang, entryKey
1216
1300
  }
1217
1301
  };
1218
1302
 
1219
- }
1303
+ }