@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.
- package/README.md +77 -0
- package/package.json +1 -1
- package/src/govcyTaskListHandler.mjs +0 -0
- package/src/middleware/govcyFormsPostHandler.mjs +10 -0
- package/src/middleware/govcyPageHandler.mjs +6 -0
- package/src/middleware/govcyReviewPostHandler.mjs +6 -0
- package/src/middleware/govcyServiceEligibilityHandler.mjs +3 -3
- package/src/middleware/govcyTaskListHandler.mjs +560 -0
- package/src/resources/govcyResources.mjs +94 -10
- package/src/utils/govcyCustomPages.mjs +65 -23
- package/src/utils/govcyDataLayer.mjs +37 -12
- package/src/utils/govcySubmitData.mjs +10 -1
- package/src/utils/govcyTaskList.mjs +284 -0
|
@@ -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
|
+
}
|