@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
|
@@ -1,4 +1,12 @@
|
|
|
1
|
-
import * as govcyResources from "../resources/govcyResources.mjs";
|
|
1
|
+
import * as govcyResources from "../resources/govcyResources.mjs";
|
|
2
|
+
|
|
3
|
+
const ALLOWED_TASK_STATUSES = new Set(["NOT_STARTED", "IN_PROGRESS", "COMPLETED"]);
|
|
4
|
+
|
|
5
|
+
function normalizeTaskStatus(statusKey = "NOT_STARTED") {
|
|
6
|
+
if (typeof statusKey !== "string") return "NOT_STARTED";
|
|
7
|
+
const normalized = statusKey.toUpperCase();
|
|
8
|
+
return ALLOWED_TASK_STATUSES.has(normalized) ? normalized : "NOT_STARTED";
|
|
9
|
+
}
|
|
2
10
|
|
|
3
11
|
/**
|
|
4
12
|
* Defines custom pages for a given siteId and pageUrl.
|
|
@@ -9,21 +17,23 @@ import * as govcyResources from "../resources/govcyResources.mjs";
|
|
|
9
17
|
* @param {string} pageTitle The title of the custom page (e.g., `{ en: "My custom section", el: "Προσαρμοσμένη ενότητα" }`)
|
|
10
18
|
* @param {string} insertAfterPageUrl The page URL to insert the custom page after (e.g., `"qualifications"`)
|
|
11
19
|
* @param {array} errors An array of error objects (e.g., `[{ id: "error1", text: { en: "Error 1", el: "Error 1"} }, { id: "error2", text: { en: "Error 2", el: "Error 2"} }]`)
|
|
12
|
-
* @param {array} summaryElements An array of summary element objects (e.g., `{ key: { en: "Extra value", el: "Πρόσθετη τιμή" }, value: [] }` )
|
|
13
|
-
* @param {boolean} summaryHtml Optional HTML content for the summary (e.g., `{ en: "<strong>Summary HTML</strong>", el: "<strong>Περίληψη HTML</strong>" }`)
|
|
14
|
-
* @param {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
20
|
+
* @param {array} summaryElements An array of summary element objects (e.g., `{ key: { en: "Extra value", el: "Πρόσθετη τιμή" }, value: [] }` )
|
|
21
|
+
* @param {boolean} summaryHtml Optional HTML content for the summary (e.g., `{ en: "<strong>Summary HTML</strong>", el: "<strong>Περίληψη HTML</strong>" }`)
|
|
22
|
+
* @param {string} taskStatus Optional default task status for task lists (NOT_STARTED/IN_PROGRESS/COMPLETED)
|
|
23
|
+
* @param {object} [extraProps={}] Optional extra metadata (e.g., `{ nextPage: "confirmation", requiresOTP: true }`)
|
|
24
|
+
*/
|
|
25
|
+
export function defineCustomPages(
|
|
26
|
+
store,
|
|
27
|
+
siteId,
|
|
19
28
|
pageUrl,
|
|
20
29
|
pageTitle,
|
|
21
30
|
insertAfterPageUrl,
|
|
22
31
|
errors,
|
|
23
|
-
summaryElements,
|
|
24
|
-
summaryHtml = false,
|
|
25
|
-
|
|
26
|
-
|
|
32
|
+
summaryElements,
|
|
33
|
+
summaryHtml = false,
|
|
34
|
+
taskStatus = "NOT_STARTED",
|
|
35
|
+
extraProps = {}
|
|
36
|
+
) {
|
|
27
37
|
// Initialize custom pages in session if not already set
|
|
28
38
|
store.siteData ??= {};
|
|
29
39
|
store.siteData[siteId] ??= {};
|
|
@@ -44,15 +54,16 @@ export function defineCustomPages(
|
|
|
44
54
|
let summaryActions = [];
|
|
45
55
|
|
|
46
56
|
// Base definition
|
|
47
|
-
const definition = {
|
|
48
|
-
pageTitle,
|
|
49
|
-
insertAfterPageUrl,
|
|
50
|
-
summaryElements: summaryElements || [],
|
|
51
|
-
errors: normalizedErrors,
|
|
52
|
-
summaryActions: summaryActions,
|
|
53
|
-
|
|
54
|
-
...
|
|
55
|
-
|
|
57
|
+
const definition = {
|
|
58
|
+
pageTitle,
|
|
59
|
+
insertAfterPageUrl,
|
|
60
|
+
summaryElements: summaryElements || [],
|
|
61
|
+
errors: normalizedErrors,
|
|
62
|
+
summaryActions: summaryActions,
|
|
63
|
+
taskStatus: normalizeTaskStatus(taskStatus),
|
|
64
|
+
...(summaryHtml ? { summaryHtml } : {}),
|
|
65
|
+
...extraProps // ✅ Merge any additional developer-defined properties
|
|
66
|
+
};
|
|
56
67
|
|
|
57
68
|
// Construct default summaryActions
|
|
58
69
|
if (pageTitle && pageUrl) {
|
|
@@ -234,7 +245,7 @@ export function clearCustomPageErrors(store, siteId, pageUrl) {
|
|
|
234
245
|
* @param {string} errorId - Unique identifier for the error.
|
|
235
246
|
* @param {Object} errorTextObject - Multilingual Object containing localized error texts.
|
|
236
247
|
*/
|
|
237
|
-
export function addCustomPageError(store, siteId, pageUrl, errorId, errorTextObject) {
|
|
248
|
+
export function addCustomPageError(store, siteId, pageUrl, errorId, errorTextObject) {
|
|
238
249
|
const normalizedPageUrl = pageUrl.replace(/^\/+/, "");
|
|
239
250
|
|
|
240
251
|
const target =
|
|
@@ -257,4 +268,35 @@ export function addCustomPageError(store, siteId, pageUrl, errorId, errorTextObj
|
|
|
257
268
|
pageUrl: normalizedPageUrl,
|
|
258
269
|
});
|
|
259
270
|
|
|
260
|
-
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Sets the task status for a custom page so task-list pages can show progress.
|
|
275
|
+
*
|
|
276
|
+
* @param {object} store Session store (req.session)
|
|
277
|
+
* @param {string} siteId Current site id
|
|
278
|
+
* @param {string} pageUrl Custom page url
|
|
279
|
+
* @param {string} statusKey One of NOT_STARTED/IN_PROGRESS/COMPLETED
|
|
280
|
+
*/
|
|
281
|
+
export function setCustomPageTaskStatus(store, siteId, pageUrl, statusKey = "NOT_STARTED") {
|
|
282
|
+
const target = store?.siteData?.[siteId]?.customPages?.[pageUrl];
|
|
283
|
+
if (!target) {
|
|
284
|
+
console.warn(`⚠️ setCustomPageTaskStatus: page '${pageUrl}' not found for site '${siteId}'`);
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
target.taskStatus = normalizeTaskStatus(statusKey);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Retrieves the stored task status for a custom page.
|
|
292
|
+
*
|
|
293
|
+
* @param {object} store Session store (req.session)
|
|
294
|
+
* @param {string} siteId Current site id
|
|
295
|
+
* @param {string} pageUrl Custom page url
|
|
296
|
+
* @returns {string} Task status key
|
|
297
|
+
*/
|
|
298
|
+
export function getCustomPageTaskStatus(store, siteId, pageUrl) {
|
|
299
|
+
const target = store?.siteData?.[siteId]?.customPages?.[pageUrl];
|
|
300
|
+
if (!target) return "NOT_STARTED";
|
|
301
|
+
return normalizeTaskStatus(target.taskStatus);
|
|
302
|
+
}
|
|
@@ -106,18 +106,43 @@ export function storePageValidationErrors(store, siteId, pageUrl, validationErro
|
|
|
106
106
|
* @param {string} pageUrl The page url
|
|
107
107
|
* @param {object} formData The form data to be stored
|
|
108
108
|
*/
|
|
109
|
-
export function storePageData(store, siteId, pageUrl, formData) {
|
|
110
|
-
// Ensure session structure is initialized
|
|
111
|
-
initializeSiteData(store, siteId, pageUrl);
|
|
112
|
-
|
|
113
|
-
store.siteData[siteId].inputData[pageUrl]["formData"] = formData;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
109
|
+
export function storePageData(store, siteId, pageUrl, formData) {
|
|
110
|
+
// Ensure session structure is initialized
|
|
111
|
+
initializeSiteData(store, siteId, pageUrl);
|
|
112
|
+
|
|
113
|
+
store.siteData[siteId].inputData[pageUrl]["formData"] = formData;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Marks whether a page has been posted/submitted at least once.
|
|
118
|
+
*
|
|
119
|
+
* @param {object} store Session store (req.session)
|
|
120
|
+
* @param {string} siteId Site id
|
|
121
|
+
* @param {string} pageUrl Page url
|
|
122
|
+
* @param {boolean} posted Flag indicating post action (defaults to true)
|
|
123
|
+
*/
|
|
124
|
+
export function setPagePosted(store, siteId, pageUrl, posted = true) {
|
|
125
|
+
initializeSiteData(store, siteId, pageUrl);
|
|
126
|
+
store.siteData[siteId].inputData[pageUrl]["posted"] = Boolean(posted);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Returns true if the user has already posted/submitted the page.
|
|
131
|
+
*
|
|
132
|
+
* @param {object} store Session store (req.session)
|
|
133
|
+
* @param {string} siteId Site id
|
|
134
|
+
* @param {string} pageUrl Page url
|
|
135
|
+
* @returns {boolean}
|
|
136
|
+
*/
|
|
137
|
+
export function isPagePosted(store, siteId, pageUrl) {
|
|
138
|
+
return Boolean(store?.siteData?.[siteId]?.inputData?.[pageUrl]?.posted);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function storePageDataElement(store, siteId, pageUrl, elementName, value) {
|
|
142
|
+
// Ensure session structure is initialized
|
|
143
|
+
initializeSiteData(store, siteId, pageUrl);
|
|
144
|
+
|
|
145
|
+
// Store the element value
|
|
121
146
|
store.siteData[siteId].inputData[pageUrl].formData[elementName] = value;
|
|
122
147
|
}
|
|
123
148
|
/**
|
|
@@ -42,6 +42,11 @@ export function prepareSubmissionData(req, siteId, service) {
|
|
|
42
42
|
|
|
43
43
|
// Loop through every page in the service definition
|
|
44
44
|
for (const page of service.pages) {
|
|
45
|
+
|
|
46
|
+
// ----- taskList handling
|
|
47
|
+
if (page.taskList) {
|
|
48
|
+
continue; // Task list pages do not contribute submission payloads
|
|
49
|
+
}
|
|
45
50
|
const pageUrl = page.pageData.url || "";
|
|
46
51
|
|
|
47
52
|
// Find the <form> element in the page
|
|
@@ -347,12 +352,16 @@ export function preparePrintFriendlyData(req, siteId, service) {
|
|
|
347
352
|
if (conditionResult.result === false) {
|
|
348
353
|
continue; // ⛔ Skip this page from print-friendly data
|
|
349
354
|
}
|
|
355
|
+
// ----- taskList handling
|
|
356
|
+
if (page.taskList) {
|
|
357
|
+
continue; // Task list hub is a navigation shell, no print-friendly entry
|
|
358
|
+
}
|
|
350
359
|
|
|
351
360
|
let pageTemplate = page.pageTemplate;
|
|
352
361
|
let pageTitle = page.pageData.title || {};
|
|
353
362
|
|
|
354
363
|
|
|
355
|
-
// -----
|
|
364
|
+
// ----- updateMyDetails hub handling
|
|
356
365
|
if (page.updateMyDetails) {
|
|
357
366
|
// create the page template
|
|
358
367
|
pageTemplate = createUmdManualPageTemplate(siteId, service.site.lang, page, req);
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import { getPageConfigData } from "./govcyLoadConfigData.mjs";
|
|
2
|
+
import { evaluatePageConditions } from "./govcyExpressions.mjs";
|
|
3
|
+
import { validateFormElements } from "./govcyValidator.mjs";
|
|
4
|
+
import { validateMultipleThings } from "./govcyMultipleThingsValidation.mjs";
|
|
5
|
+
import { createUmdManualPageTemplate } from "../middleware/govcyUpdateMyDetails.mjs";
|
|
6
|
+
import * as dataLayer from "./govcyDataLayer.mjs";
|
|
7
|
+
import { logger } from "./govcyLogger.mjs";
|
|
8
|
+
import * as govcyResources from "../resources/govcyResources.mjs";
|
|
9
|
+
import { getCustomPageTaskStatus } from "./govcyCustomPages.mjs";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Computes the completion status for a single page.
|
|
13
|
+
* Mirrors review-stage validation so task lists use the same rules.
|
|
14
|
+
*/
|
|
15
|
+
export function computePageTaskStatus(req, siteId, service, pageUrl, visitedPages = new Set()) {
|
|
16
|
+
if (!req || !req.session) {
|
|
17
|
+
logger.error("computePageTaskStatus called without session", { siteId, pageUrl });
|
|
18
|
+
throw new Error("computePageTaskStatus: request with session is required");
|
|
19
|
+
}
|
|
20
|
+
if (!service || !Array.isArray(service.pages)) {
|
|
21
|
+
logger.error("computePageTaskStatus service missing pages array", { siteId, pageUrl });
|
|
22
|
+
throw new Error("computePageTaskStatus: service.pages is required");
|
|
23
|
+
}
|
|
24
|
+
if (!pageUrl) {
|
|
25
|
+
logger.error("computePageTaskStatus missing pageUrl", { siteId });
|
|
26
|
+
throw new Error("computePageTaskStatus: pageUrl is required");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Handle custom pages first
|
|
30
|
+
const customPage = dataLayer.getSiteCustomPages(req.session, siteId)?.[pageUrl];
|
|
31
|
+
if (customPage) {
|
|
32
|
+
return {
|
|
33
|
+
pageUrl,
|
|
34
|
+
title: customPage?.pageTitle || {},
|
|
35
|
+
type: "custom",
|
|
36
|
+
status: getCustomPageTaskStatus(req.session, siteId, pageUrl),
|
|
37
|
+
hasData: Boolean(customPage?.data),
|
|
38
|
+
custom: true
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const page = getPageConfigData(service, pageUrl);
|
|
43
|
+
if (!page) {
|
|
44
|
+
logger.error("computePageTaskStatus page not found", { siteId, pageUrl });
|
|
45
|
+
throw new Error("computePageTaskStatus: page '" + pageUrl + "' not found in service configuration");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Use a composite key so nested task-list evaluation can detect circular references.
|
|
49
|
+
const cycleKey = siteId + ":" + pageUrl;
|
|
50
|
+
if (visitedPages.has(cycleKey)) {
|
|
51
|
+
const cyclePath = [...visitedPages, cycleKey].map(key => key.split(":")[1]);
|
|
52
|
+
logger.error("computePageTaskStatus circular task list reference detected", {
|
|
53
|
+
siteId,
|
|
54
|
+
cycle: cyclePath
|
|
55
|
+
});
|
|
56
|
+
throw new Error("Task list configuration contains a circular reference: " + cyclePath.join(" -> "));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
visitedPages.add(cycleKey);
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
// Reuse the same condition logic as page rendering – if a page would redirect,
|
|
63
|
+
// we treat it as skipped so task lists never block on hidden sections.
|
|
64
|
+
const conditionResult = evaluatePageConditions(page, req.session, siteId, req);
|
|
65
|
+
if (conditionResult?.result === false) {
|
|
66
|
+
return buildStatusResult(page, determinePageType(page), pageUrl, false, "SKIPPED", conditionResult);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const pageType = determinePageType(page);
|
|
70
|
+
if (pageType === "taskList") {
|
|
71
|
+
// For task list pages, recurse into each configured children and aggregate their status.
|
|
72
|
+
const taskPages = Array.isArray(page?.taskList?.taskPages) ? page.taskList.taskPages : [];
|
|
73
|
+
const summary = computeTaskListStatus(req, siteId, service, taskPages, visitedPages);
|
|
74
|
+
const result = buildStatusResult(page, pageType, pageUrl, summary.status !== "NOT_STARTED", summary.status);
|
|
75
|
+
result.taskList = summary;
|
|
76
|
+
return result;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Below covers all data-entry pages (normal, multipleThings, updateMyDetails)
|
|
80
|
+
// For form-based pages, inspect the session data and validation results.
|
|
81
|
+
let storedData = dataLayer.getPageData(req.session, siteId, pageUrl);
|
|
82
|
+
const postedFlag = dataLayer.isPagePosted(req.session, siteId, pageUrl);
|
|
83
|
+
|
|
84
|
+
// Multiple things hubs always expect an array, so normalize bad shapes.
|
|
85
|
+
if (pageType === "multipleThings" && !Array.isArray(storedData)) {
|
|
86
|
+
storedData = [];
|
|
87
|
+
}
|
|
88
|
+
// All other page types expect an object. When nothing is stored yet, fall back to {}.
|
|
89
|
+
if (!storedData || typeof storedData !== "object") {
|
|
90
|
+
storedData = Array.isArray(storedData) ? storedData : {};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const hasData = determineHasData(storedData, pageType, postedFlag);
|
|
94
|
+
if (!hasData) {
|
|
95
|
+
return buildStatusResult(page, pageType, pageUrl, false, "NOT_STARTED");
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// For form-based pages, inspect the session data and validation results.
|
|
99
|
+
const hasErrors = hasValidationErrors({
|
|
100
|
+
page,
|
|
101
|
+
pageType,
|
|
102
|
+
storedData,
|
|
103
|
+
req,
|
|
104
|
+
service,
|
|
105
|
+
siteId
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
return buildStatusResult(
|
|
109
|
+
page,
|
|
110
|
+
pageType,
|
|
111
|
+
pageUrl,
|
|
112
|
+
true,
|
|
113
|
+
hasErrors ? "IN_PROGRESS" : "COMPLETED"
|
|
114
|
+
);
|
|
115
|
+
} finally {
|
|
116
|
+
visitedPages.delete(cycleKey);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Computes statuses for all pages listed in taskPages and derives overall status.
|
|
122
|
+
*/
|
|
123
|
+
export function computeTaskListStatus(req, siteId, service, taskPages = [], visitedPages = new Set()) {
|
|
124
|
+
if (!Array.isArray(taskPages)) {
|
|
125
|
+
logger.error("computeTaskListStatus expects taskPages array", { siteId });
|
|
126
|
+
throw new Error("taskPages must be an array");
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const tasks = taskPages.map(pageUrl => computePageTaskStatus(req, siteId, service, pageUrl, visitedPages));
|
|
130
|
+
const status = deriveTaskListStatus(tasks);
|
|
131
|
+
return { status, tasks };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Returns a normalized type string so callers can branch per page category.
|
|
136
|
+
*
|
|
137
|
+
* @param {object} page Page configuration object
|
|
138
|
+
* @returns {"taskList"|"updateMyDetails"|"multipleThings"|"normal"}
|
|
139
|
+
*/
|
|
140
|
+
function determinePageType(page) {
|
|
141
|
+
if (page?.taskList) return "taskList";
|
|
142
|
+
if (page?.updateMyDetails) return "updateMyDetails";
|
|
143
|
+
if (page?.multipleThings) return "multipleThings";
|
|
144
|
+
return "normal";
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Determines whether the stored data should count as "started".
|
|
149
|
+
*
|
|
150
|
+
* @param {object|Array} storedData Data retrieved from the session
|
|
151
|
+
* @param {string} pageType Derived page type
|
|
152
|
+
* @param {boolean} postedFlag For multipleThings pages, whether user pressed continue at least once
|
|
153
|
+
* @returns {boolean}
|
|
154
|
+
*/
|
|
155
|
+
function determineHasData(storedData, pageType, postedFlag = false) {
|
|
156
|
+
|
|
157
|
+
if (Array.isArray(storedData)) {
|
|
158
|
+
if (storedData.length > 0) return true;
|
|
159
|
+
return pageType === "multipleThings" && postedFlag;
|
|
160
|
+
}
|
|
161
|
+
if (storedData && typeof storedData === "object") {
|
|
162
|
+
const hasKeys = Object.keys(storedData).length > 0;
|
|
163
|
+
if (hasKeys) return true;
|
|
164
|
+
}
|
|
165
|
+
if (pageType === "multipleThings" && postedFlag) {
|
|
166
|
+
return true;
|
|
167
|
+
}
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Mirrors review validation to decide if a page currently has outstanding errors.
|
|
173
|
+
*
|
|
174
|
+
* @param {object} opts Inputs bundle
|
|
175
|
+
* @returns {boolean}
|
|
176
|
+
*/
|
|
177
|
+
function hasValidationErrors({ page, pageType, storedData, req, service, siteId }) {
|
|
178
|
+
if (pageType === "multipleThings") {
|
|
179
|
+
const lang = service?.site?.lang || req?.globalLang || "el";
|
|
180
|
+
const errors = validateMultipleThings(page, storedData, lang);
|
|
181
|
+
return Object.keys(errors || {}).length > 0;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const formElement = getFormElementForPage({ page, pageType, req, service, siteId });
|
|
185
|
+
if (!formElement) {
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const errors = validateFormElements(
|
|
190
|
+
formElement.params?.elements || [],
|
|
191
|
+
storedData,
|
|
192
|
+
page?.pageData?.url
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
return Object.keys(errors || {}).length > 0;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Retrieves a form element definition so the validator can run against it.
|
|
200
|
+
*
|
|
201
|
+
* @param {object} params Inputs bundle
|
|
202
|
+
* @returns {object|null}
|
|
203
|
+
*/
|
|
204
|
+
function getFormElementForPage({ page, pageType, req, service, siteId }) {
|
|
205
|
+
if (pageType === "updateMyDetails") {
|
|
206
|
+
const lang = service?.site?.lang || req?.globalLang || "el";
|
|
207
|
+
const template = createUmdManualPageTemplate(siteId, lang, page, req, true);
|
|
208
|
+
return findFormElement(template?.sections);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return findFormElement(page?.pageTemplate?.sections);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Depth-first scan for the first form element within renderer sections.
|
|
216
|
+
*
|
|
217
|
+
* @param {Array} sections Renderer sections array
|
|
218
|
+
* @returns {object|null}
|
|
219
|
+
*/
|
|
220
|
+
function findFormElement(sections = []) {
|
|
221
|
+
if (!Array.isArray(sections)) return null;
|
|
222
|
+
for (const section of sections) {
|
|
223
|
+
const elements = section?.elements || [];
|
|
224
|
+
for (const element of elements) {
|
|
225
|
+
if (element?.element === "form") {
|
|
226
|
+
return element;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Constructs the canonical payload returned by computePageTaskStatus.
|
|
235
|
+
*
|
|
236
|
+
* @param {object} page Page config
|
|
237
|
+
* @param {string} pageType Derived type string
|
|
238
|
+
* @param {string} pageUrl Page identifier
|
|
239
|
+
* @param {boolean} hasData Whether any user input exists
|
|
240
|
+
* @param {string} status Status flag
|
|
241
|
+
* @param {object|null} conditionResult Optional condition evaluation details
|
|
242
|
+
* @returns {object}
|
|
243
|
+
*/
|
|
244
|
+
function buildStatusResult(page, pageType, pageUrl, hasData, status, conditionResult = null) {
|
|
245
|
+
const result = {
|
|
246
|
+
pageUrl,
|
|
247
|
+
title: pageType === "updateMyDetails"
|
|
248
|
+
? govcyResources.staticResources.text.updateMyDetailsTitle
|
|
249
|
+
: (page?.pageData?.title || {}),
|
|
250
|
+
type: pageType,
|
|
251
|
+
status,
|
|
252
|
+
hasData
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
if (conditionResult) {
|
|
256
|
+
result.conditionResult = conditionResult;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return result;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Collapses individual task statuses into a single overall status.
|
|
264
|
+
*
|
|
265
|
+
* @param {Array<{status:string}>} tasks Task descriptors
|
|
266
|
+
* @returns {"NOT_STARTED"|"IN_PROGRESS"|"COMPLETED"}
|
|
267
|
+
*/
|
|
268
|
+
function deriveTaskListStatus(tasks = []) {
|
|
269
|
+
const relevant = tasks.filter(task => task.status !== "SKIPPED");
|
|
270
|
+
if (relevant.length === 0) return "COMPLETED";
|
|
271
|
+
|
|
272
|
+
const statuses = relevant.map(task => task.status);
|
|
273
|
+
const hasNotStarted = statuses.includes("NOT_STARTED");
|
|
274
|
+
const hasCompleted = statuses.includes("COMPLETED");
|
|
275
|
+
const hasInProgress = statuses.includes("IN_PROGRESS");
|
|
276
|
+
|
|
277
|
+
if (hasInProgress || (hasCompleted && hasNotStarted)) {
|
|
278
|
+
return "IN_PROGRESS";
|
|
279
|
+
}
|
|
280
|
+
if (hasNotStarted) {
|
|
281
|
+
return "NOT_STARTED";
|
|
282
|
+
}
|
|
283
|
+
return "COMPLETED";
|
|
284
|
+
}
|