@gov-cy/govcy-express-services 1.3.1 → 1.4.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 +223 -0
- package/package.json +7 -2
- package/src/index.mjs +47 -7
- package/src/middleware/govcyReviewPageHandler.mjs +4 -1
- package/src/middleware/govcyReviewPostHandler.mjs +70 -14
- package/src/utils/govcyCustomPages.mjs +260 -0
- package/src/utils/govcyDataLayer.mjs +17 -0
- package/src/utils/govcySubmitData.mjs +175 -90
package/README.md
CHANGED
|
@@ -43,6 +43,7 @@ The APIs used for submission, temporary save and file uploads are not part of th
|
|
|
43
43
|
- [🔀 Conditional logic](#-conditional-logic)
|
|
44
44
|
- [💾 Temporary save feature](#-temporary-save-feature)
|
|
45
45
|
- [🗃️ Files uploads feature](#%EF%B8%8F-files-uploads-feature)
|
|
46
|
+
- [✨ Custom pages feature](#-custom-pages-feature)
|
|
46
47
|
- [🛣️ Routes](#%EF%B8%8F-routes)
|
|
47
48
|
- [👨💻 Environment variables](#-environment-variables)
|
|
48
49
|
- [🔒 Security note](#-security-note)
|
|
@@ -2603,6 +2604,228 @@ To help back-end systems recognize the field as a file, the field's element name
|
|
|
2603
2604
|
If these endpoints are not defined in the service JSON, the file upload logic is skipped entirely.
|
|
2604
2605
|
Existing services will continue to work without modification.
|
|
2605
2606
|
|
|
2607
|
+
### ✨ Custom pages feature
|
|
2608
|
+
The **Custom pages** feature allows developers to add service-specific custom pages that exist outside the standard Express Services JSON configuration.
|
|
2609
|
+
|
|
2610
|
+
These pages can collect data, display conditional content, and inject custom sections into the **review** and **email** stages of the service flow.
|
|
2611
|
+
|
|
2612
|
+
#### What do custom pages get out of the framework
|
|
2613
|
+
With the custom pages feature, developers can define **pages** and **routes** on an express **service**. On these pages the following apply out of the box:
|
|
2614
|
+
- Login requirement
|
|
2615
|
+
- User policy requirement
|
|
2616
|
+
- Any service eligibility checks
|
|
2617
|
+
- Csrf protection on POST
|
|
2618
|
+
- Generic error page on errors
|
|
2619
|
+
|
|
2620
|
+
The developers can also choose to:
|
|
2621
|
+
- store values in the session to be submitted via the submission API
|
|
2622
|
+
- define where and what the users sees in the `review` page
|
|
2623
|
+
- define errors regarding the custom page in the `review` page
|
|
2624
|
+
- define what the user receives the `email`
|
|
2625
|
+
|
|
2626
|
+
#### What the framework expects from custom pages
|
|
2627
|
+
|
|
2628
|
+
- Every custom page **must be defined** at startup using `defineCustomPages(app, siteId, pageUrl, ...)`.
|
|
2629
|
+
- Custom pages are **not** automatically discovered, they must be registered explicitly.
|
|
2630
|
+
- Each page must specify on **start up**:
|
|
2631
|
+
- `pageTitle`: multilingual object for display in review and email
|
|
2632
|
+
- `insertAfterPageUrl`: the page **after which** this custom page will appear
|
|
2633
|
+
- `summaryElements`, `errors`, and `email` are managed dynamically in session
|
|
2634
|
+
- **NOTE**: Usually a default `required` error must be defined on start up. This will ensure that the when the users try to submit in the “Check your answers” review page, they get an error message to complete an action.
|
|
2635
|
+
- `extraProps` extra property stored in the session to be used by the developers as they wish
|
|
2636
|
+
- During **runtime**, developers are responsible for setting:
|
|
2637
|
+
- `data`: data to include in submission, using the `setCustomPageData` function
|
|
2638
|
+
- `summaryElements`: what appears in the “Check your answers” review page, using the `setCustomPageSummaryElements` function
|
|
2639
|
+
- `email`: array of [`dsf-email-templates`](https://github.com/gov-cy/dsf-email-templates) components for the submission confirmation email, using the `setCustomPageEmail`
|
|
2640
|
+
- `errors`: errors that appear in the “Check your answers” review page. Developers can use the `addCustomPageError` function to add an error, or the `clearCustomPageErrors` to clear the errors
|
|
2641
|
+
#### Available methods
|
|
2642
|
+
|
|
2643
|
+
| Method | Purpose |
|
|
2644
|
+
| ------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------- |
|
|
2645
|
+
| `defineCustomPages(store, siteId, pageUrl, pageTitle, insertAfterSummary, insertAfterPageUrl, errors, summaryElements, summaryHtml, extraProps)` | Registers a custom page definition in `app`. Must be called once at startup. |
|
|
2646
|
+
| `getCustomPageDefinition(store, siteId, pageUrl)` | Retrieves the custom page definition for a given siteId and pageUrl |
|
|
2647
|
+
| `resetCustomPages(configStore, store, siteId)` | Resets per-session data from the global definitions. |
|
|
2648
|
+
| `setCustomPageData(store, siteId, pageUrl, dataObject)` | Sets or replaces the data object used during submission. |
|
|
2649
|
+
| `setCustomPageSummaryElements(store, siteId, pageUrl, summaryElements)` | Sets what appears in the review page summary. |
|
|
2650
|
+
| `clearCustomPageErrors(store, siteId, pageUrl)` | Clears validation errors. |
|
|
2651
|
+
| `addCustomPageError(store, siteId, pageUrl, errorId, errorTextObject)` | Adds a validation error. |
|
|
2652
|
+
| `setCustomPageEmail(store, siteId, pageUrl, arrayOfEmailObjects)` | Defines email sections for the confirmation email using `dsf-email-templates` components. |
|
|
2653
|
+
| `setCustomPageProperty(store, siteId, pageUrl, property, value, isDefinition = false)` | Sets a custom property on a given custom page definition or instance |
|
|
2654
|
+
| `getCustomPageProperty(store, siteId, pageUrl, property, isDefinition = false)` | Gets a custom property from a given custom page definition or instance. |
|
|
2655
|
+
|
|
2656
|
+
---
|
|
2657
|
+
##### Example
|
|
2658
|
+
|
|
2659
|
+
Below is a practical example of a custom **Declarations** page added to an existing service.
|
|
2660
|
+
|
|
2661
|
+
```js
|
|
2662
|
+
import initializeGovCyExpressService from '@gov-cy/govcy-express-services';
|
|
2663
|
+
import {
|
|
2664
|
+
defineCustomPages, resetCustomPages,
|
|
2665
|
+
clearCustomPageErrors, addCustomPageError,
|
|
2666
|
+
setCustomPageData, setCustomPageSummaryElements,
|
|
2667
|
+
setCustomPageEmail, getCustomPageProperty, setCustomPageProperty
|
|
2668
|
+
} from '@gov-cy/govcy-express-services/customPages';
|
|
2669
|
+
|
|
2670
|
+
// Initialize the service
|
|
2671
|
+
const service = initializeGovCyExpressService({
|
|
2672
|
+
beforeMount({ siteRoute, app }) {
|
|
2673
|
+
|
|
2674
|
+
// ==========================================================
|
|
2675
|
+
// 1️⃣ DEFINE GLOBAL CUSTOM PAGE CONFIGS (once per app)
|
|
2676
|
+
// ==========================================================
|
|
2677
|
+
defineCustomPages(
|
|
2678
|
+
app, // <-- using app as store (global config)
|
|
2679
|
+
"cso", // siteId
|
|
2680
|
+
"/cso/custom", // pageUrl
|
|
2681
|
+
{ en: "My custom section", el: "Προσαρμοσμένη ενότητα" }, // pageTitle
|
|
2682
|
+
"qualifications", // insertAfterPageUrl
|
|
2683
|
+
[ // errors
|
|
2684
|
+
{
|
|
2685
|
+
id: "custom-error",
|
|
2686
|
+
text: {
|
|
2687
|
+
en: "This is a custom error custom",
|
|
2688
|
+
el: "Αυτή ειναι ενα προσαρμοσμένη σφαλμα custom",
|
|
2689
|
+
}
|
|
2690
|
+
}
|
|
2691
|
+
],
|
|
2692
|
+
[ // summaryElements
|
|
2693
|
+
{
|
|
2694
|
+
key:
|
|
2695
|
+
{
|
|
2696
|
+
en: "Extra value",
|
|
2697
|
+
el: "Πρόσθετη τιμή"
|
|
2698
|
+
},
|
|
2699
|
+
value: []
|
|
2700
|
+
}
|
|
2701
|
+
],
|
|
2702
|
+
false,
|
|
2703
|
+
{ // other custom properties if needed like nextPage or initial data
|
|
2704
|
+
nextPage: "/cso/memberships", // custom property nextPage. Not needed by Express but useful for the custom logic
|
|
2705
|
+
data : // custom initial data. Useful when you need the data model to be standard or pre-populated
|
|
2706
|
+
{
|
|
2707
|
+
something: "",
|
|
2708
|
+
}
|
|
2709
|
+
}
|
|
2710
|
+
);
|
|
2711
|
+
|
|
2712
|
+
// ==========================================================
|
|
2713
|
+
// 2️⃣ MIDDLEWARE: SET UP SESSION DATA FROM GLOBAL DEFINITIONS
|
|
2714
|
+
// ==========================================================
|
|
2715
|
+
app.use((req, res, next) => {
|
|
2716
|
+
// Initialize session data for custom pages
|
|
2717
|
+
req.session.siteData ??= {};
|
|
2718
|
+
req.session.siteData["cso"] ??= {};
|
|
2719
|
+
|
|
2720
|
+
// Reset session copies if missing (first visit)
|
|
2721
|
+
if (!req.session.siteData["cso"].customPages) {
|
|
2722
|
+
resetCustomPages(app, req.session, "cso"); // 🔁 deep copy from app to session
|
|
2723
|
+
req.session.save((err) => {
|
|
2724
|
+
if (err) console.error("⚠️ Error initializing customPages:", err);
|
|
2725
|
+
});
|
|
2726
|
+
}
|
|
2727
|
+
|
|
2728
|
+
next();
|
|
2729
|
+
});
|
|
2730
|
+
|
|
2731
|
+
// ==========================================================
|
|
2732
|
+
// 3️⃣ CUSTOM ROUTES (still per-user)
|
|
2733
|
+
// ==========================================================
|
|
2734
|
+
|
|
2735
|
+
// GET `/cso/custom`
|
|
2736
|
+
siteRoute("cso", "get", "/cso/custom", (req, res) => {
|
|
2737
|
+
|
|
2738
|
+
// Render the custom page using the session data
|
|
2739
|
+
// It is important for POST to add the csrfToken and to pass on the route query parameter
|
|
2740
|
+
res.send(`<form action="/cso/custom${(req.query.route === "review")?"?route=review":""}" method="post">
|
|
2741
|
+
custom for ${req.params.siteId}
|
|
2742
|
+
<input type="hidden" name="_csrf" value="${req.csrfToken()}">
|
|
2743
|
+
<button type="submit">Submit custom page</button>
|
|
2744
|
+
</form>`);
|
|
2745
|
+
|
|
2746
|
+
});
|
|
2747
|
+
|
|
2748
|
+
// POST `/cso/custom`
|
|
2749
|
+
siteRoute("cso", "post", "/cso/custom", (req, res) => {
|
|
2750
|
+
|
|
2751
|
+
// Update custom page `data` dynamically`
|
|
2752
|
+
setCustomPageData(req.session, "cso", "/cso/custom", {
|
|
2753
|
+
something: "123",
|
|
2754
|
+
});
|
|
2755
|
+
|
|
2756
|
+
// Update `summary elements` dynamically (example)
|
|
2757
|
+
setCustomPageSummaryElements(req.session, "cso", "/cso/custom",
|
|
2758
|
+
[
|
|
2759
|
+
{
|
|
2760
|
+
key: {
|
|
2761
|
+
en: "Extra value",
|
|
2762
|
+
el: "Πρόσθετη τιμή"
|
|
2763
|
+
},
|
|
2764
|
+
value: [
|
|
2765
|
+
{
|
|
2766
|
+
element: "textElement",
|
|
2767
|
+
params: {
|
|
2768
|
+
text: {
|
|
2769
|
+
en: "123 Changed",
|
|
2770
|
+
el: "123 Αλλάχθηκε"
|
|
2771
|
+
},
|
|
2772
|
+
type: "span",
|
|
2773
|
+
showNewLine: true,
|
|
2774
|
+
}
|
|
2775
|
+
}
|
|
2776
|
+
]
|
|
2777
|
+
}
|
|
2778
|
+
]);
|
|
2779
|
+
|
|
2780
|
+
// Update the custom page `email`
|
|
2781
|
+
setCustomPageEmail(req.session, "cso", "/cso/custom", [
|
|
2782
|
+
{
|
|
2783
|
+
component: "bodyKeyValue",
|
|
2784
|
+
params: {
|
|
2785
|
+
type: "ul",
|
|
2786
|
+
items: [
|
|
2787
|
+
{ key: "Extra value", value: "123" },
|
|
2788
|
+
{ key: "Priority level", value: "High" },
|
|
2789
|
+
],
|
|
2790
|
+
},
|
|
2791
|
+
}
|
|
2792
|
+
]);
|
|
2793
|
+
|
|
2794
|
+
// clear any previous errors
|
|
2795
|
+
clearCustomPageErrors(req.session, "cso", "/cso/custom");
|
|
2796
|
+
|
|
2797
|
+
// Add a custom error
|
|
2798
|
+
// addCustomPageError(req.session, "cso", "/cso/custom", {
|
|
2799
|
+
// id: "custom-error",
|
|
2800
|
+
// text: {
|
|
2801
|
+
// en: "This is a custom error custom",
|
|
2802
|
+
// el: "Αυτή ειναι ενα προσαρμοσμένη σφαλμα custom",
|
|
2803
|
+
// }
|
|
2804
|
+
// });
|
|
2805
|
+
|
|
2806
|
+
//if route is review, redirect to review page else go to nextPage property
|
|
2807
|
+
if (req.query.route === "review") {
|
|
2808
|
+
res.redirect("/cso/review");
|
|
2809
|
+
return;
|
|
2810
|
+
} else {
|
|
2811
|
+
//example of using custom property nextPage
|
|
2812
|
+
res.redirect(getCustomPageProperty(req.session, "cso", "/cso/custom", "nextPage", false));
|
|
2813
|
+
return;
|
|
2814
|
+
}
|
|
2815
|
+
});
|
|
2816
|
+
|
|
2817
|
+
|
|
2818
|
+
// ==========================================================
|
|
2819
|
+
},
|
|
2820
|
+
});
|
|
2821
|
+
|
|
2822
|
+
|
|
2823
|
+
// Start the server
|
|
2824
|
+
service.startServer();
|
|
2825
|
+
|
|
2826
|
+
```
|
|
2827
|
+
|
|
2828
|
+
|
|
2606
2829
|
### 🛣️ Routes
|
|
2607
2830
|
The project uses express.js to serve the following routes:
|
|
2608
2831
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gov-cy/govcy-express-services",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "An Express-based system that dynamically renders services using @gov-cy/govcy-frontend-renderer and posts data to a submission API.",
|
|
5
5
|
"author": "DMRID - DSF Team",
|
|
6
6
|
"license": "MIT",
|
|
@@ -8,7 +8,12 @@
|
|
|
8
8
|
"main": "./src/index.mjs",
|
|
9
9
|
"module": "./src/index.mjs",
|
|
10
10
|
"exports": {
|
|
11
|
-
"
|
|
11
|
+
".": {
|
|
12
|
+
"import": "./src/index.mjs"
|
|
13
|
+
},
|
|
14
|
+
"./customPages": {
|
|
15
|
+
"import": "./src/utils/govcyCustomPages.mjs"
|
|
16
|
+
}
|
|
12
17
|
},
|
|
13
18
|
"files": [
|
|
14
19
|
"src"
|
package/src/index.mjs
CHANGED
|
@@ -35,7 +35,7 @@ import { logger } from "./utils/govcyLogger.mjs";
|
|
|
35
35
|
|
|
36
36
|
import fs from 'fs';
|
|
37
37
|
|
|
38
|
-
export default function initializeGovCyExpressService() {
|
|
38
|
+
export default function initializeGovCyExpressService(opts = {}) {
|
|
39
39
|
const app = express();
|
|
40
40
|
|
|
41
41
|
// Add this line before session middleware
|
|
@@ -152,6 +152,46 @@ export default function initializeGovCyExpressService() {
|
|
|
152
152
|
govcyFileUpload
|
|
153
153
|
);
|
|
154
154
|
|
|
155
|
+
// ==========================================================
|
|
156
|
+
// Custom pages
|
|
157
|
+
// ==========================================================
|
|
158
|
+
/**
|
|
159
|
+
* siteRoute helper:
|
|
160
|
+
* Registers a route NOT under /:siteId, but injects req.params.siteId manually.
|
|
161
|
+
*/
|
|
162
|
+
const siteRoute = (siteId, method, path, ...handlers) => {
|
|
163
|
+
if (typeof app[method] !== "function") {
|
|
164
|
+
throw new Error(`Unsupported HTTP method: ${method}`);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Middleware to manually inject the siteId param
|
|
168
|
+
const injectSiteId = (req, res, next) => {
|
|
169
|
+
req.params = req.params || {};
|
|
170
|
+
req.params.siteId = siteId;
|
|
171
|
+
next();
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
// Chain your standard middlewares AFTER injection
|
|
175
|
+
const wrappedHandlers = [
|
|
176
|
+
injectSiteId,
|
|
177
|
+
serviceConfigDataMiddleware,
|
|
178
|
+
requireAuth,
|
|
179
|
+
naturalPersonPolicy,
|
|
180
|
+
govcyServiceEligibilityHandler(),
|
|
181
|
+
...handlers,
|
|
182
|
+
];
|
|
183
|
+
|
|
184
|
+
// ✅ Register under a plain path (no /:siteId/)
|
|
185
|
+
// e.g. path = "/custom" → registers "/custom" only
|
|
186
|
+
app[method](path, ...wrappedHandlers);
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
// Allow custom routes
|
|
190
|
+
if (typeof opts.beforeMount === "function") {
|
|
191
|
+
opts.beforeMount({ siteRoute, app });
|
|
192
|
+
}
|
|
193
|
+
// ==========================================================
|
|
194
|
+
|
|
155
195
|
// 🗃️ -- ROUTE: Handle POST requests for file uploads inside multipleThings (edit)
|
|
156
196
|
app.post('/apis/:siteId/:pageUrl/multiple/edit/:index/upload',
|
|
157
197
|
serviceConfigDataMiddleware,
|
|
@@ -299,13 +339,13 @@ export default function initializeGovCyExpressService() {
|
|
|
299
339
|
);
|
|
300
340
|
|
|
301
341
|
// ----- `updateMyDetails` handling
|
|
302
|
-
|
|
342
|
+
|
|
303
343
|
// 🔀➡️ -- ROUTE coming from incoming update my details /:siteId/:pageUrl/update-my-details-response
|
|
304
|
-
app.post('/:siteId/:pageUrl/update-my-details-response',
|
|
305
|
-
serviceConfigDataMiddleware,
|
|
306
|
-
requireAuth,
|
|
307
|
-
naturalPersonPolicy,
|
|
308
|
-
govcyServiceEligibilityHandler(true),
|
|
344
|
+
app.post('/:siteId/:pageUrl/update-my-details-response',
|
|
345
|
+
serviceConfigDataMiddleware,
|
|
346
|
+
requireAuth,
|
|
347
|
+
naturalPersonPolicy,
|
|
348
|
+
govcyServiceEligibilityHandler(true),
|
|
309
349
|
govcyUpdateMyDetailsPostHandler());
|
|
310
350
|
// ----- `updateMyDetails` handling
|
|
311
351
|
|
|
@@ -89,7 +89,10 @@ export function govcyReviewPageHandler() {
|
|
|
89
89
|
if (fieldKey === "type") continue;
|
|
90
90
|
// Push each field error to summary items
|
|
91
91
|
summaryItems.push({
|
|
92
|
-
link:
|
|
92
|
+
link: (err.type === "custom" ? // Custom pages
|
|
93
|
+
`/${fieldErr.pageUrl}?route=review` :
|
|
94
|
+
govcyResources.constructPageUrl(siteId, fieldErr.pageUrl, "review")
|
|
95
|
+
),
|
|
93
96
|
text: fieldErr.message
|
|
94
97
|
});
|
|
95
98
|
}
|
|
@@ -10,6 +10,7 @@ import { sendEmail } from "../utils/govcyNotification.mjs"
|
|
|
10
10
|
import { evaluatePageConditions } from "../utils/govcyExpressions.mjs";
|
|
11
11
|
import { createUmdManualPageTemplate } from "./govcyUpdateMyDetails.mjs"
|
|
12
12
|
import { validateMultipleThings } from "../utils/govcyMultipleThingsValidation.mjs";
|
|
13
|
+
import { resetCustomPages } from "../utils/govcyCustomPages.mjs";
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
16
|
* Middleware to handle review page form submission
|
|
@@ -25,12 +26,12 @@ export function govcyReviewPostHandler() {
|
|
|
25
26
|
let validationErrors = {};
|
|
26
27
|
// to be used for sending email
|
|
27
28
|
let updateMyDetailsData = null;
|
|
28
|
-
|
|
29
|
+
|
|
29
30
|
// Loop through all pages in the service
|
|
30
31
|
for (const page of service.pages) {
|
|
31
32
|
//get page url
|
|
32
33
|
const pageUrl = page.pageData.url;
|
|
33
|
-
|
|
34
|
+
|
|
34
35
|
// to be used for sending email
|
|
35
36
|
// get updateMyDetails data if not found before
|
|
36
37
|
if (!updateMyDetailsData) {
|
|
@@ -52,13 +53,13 @@ export function govcyReviewPostHandler() {
|
|
|
52
53
|
if (page.updateMyDetails) {
|
|
53
54
|
logger.debug("Validating UpdateMyDetails page during review POST", { siteId, pageUrl });
|
|
54
55
|
// Build the manual UMD page template
|
|
55
|
-
const umdTemplate
|
|
56
|
-
|
|
56
|
+
const umdTemplate = createUmdManualPageTemplate(siteId, service.site.lang, page, req);
|
|
57
|
+
|
|
57
58
|
// Extract the form element
|
|
58
|
-
formElement = umdTemplate
|
|
59
|
+
formElement = umdTemplate.sections
|
|
59
60
|
.flatMap(section => section.elements)
|
|
60
61
|
.find(el => el.element === "form");
|
|
61
|
-
|
|
62
|
+
|
|
62
63
|
if (!formElement) {
|
|
63
64
|
logger.error("🚨 UMD form element not found during review validation", { siteId, pageUrl });
|
|
64
65
|
return handleMiddlewareError("🚨 UMD form element not found during review validation", 500, next);
|
|
@@ -107,6 +108,53 @@ export function govcyReviewPostHandler() {
|
|
|
107
108
|
validationErrors = { ...validationErrors, ...errors };
|
|
108
109
|
}
|
|
109
110
|
|
|
111
|
+
// ==========================================================
|
|
112
|
+
// Custom pages
|
|
113
|
+
// ==========================================================
|
|
114
|
+
// Handle custom summary validation blocks from session
|
|
115
|
+
const customPages = dataLayer.getSiteCustomPages(req.session, siteId);
|
|
116
|
+
|
|
117
|
+
// Convert existing validationErrors object to ordered array of [pageUrl, errorObj]
|
|
118
|
+
// so we can splice into it in the correct order.
|
|
119
|
+
let orderedErrors = Object.entries(validationErrors);
|
|
120
|
+
|
|
121
|
+
for (const [key, block] of Object.entries(customPages)) {
|
|
122
|
+
if (Array.isArray(block.errors) && block.errors.length > 0) {
|
|
123
|
+
const customErrObj = {};
|
|
124
|
+
for (const err of block.errors) {
|
|
125
|
+
customErrObj[err.id] = {
|
|
126
|
+
id: err.id,
|
|
127
|
+
message: err.text, // ⚙️ uses your structure
|
|
128
|
+
pageUrl: err.pageUrl || key
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const newErrorEntry = [
|
|
133
|
+
key,
|
|
134
|
+
{
|
|
135
|
+
type: "custom", // mark it as custom for debug clarity
|
|
136
|
+
...customErrObj
|
|
137
|
+
}
|
|
138
|
+
];
|
|
139
|
+
|
|
140
|
+
// If block.insertAfter exists, insert after that page’s index
|
|
141
|
+
if (block.insertAfterPageUrl) {
|
|
142
|
+
const idx = orderedErrors.findIndex(([pageUrl]) => pageUrl === block.insertAfterPageUrl);
|
|
143
|
+
if (idx >= 0) {
|
|
144
|
+
orderedErrors.splice(idx + 1, 0, newErrorEntry); // insert after
|
|
145
|
+
continue; // move to next custom block
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Fallback: append to top if insertAfter not found or not defined
|
|
150
|
+
orderedErrors.unshift(newErrorEntry);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Convert back into a normal object
|
|
155
|
+
validationErrors = Object.fromEntries(orderedErrors);
|
|
156
|
+
// ==========================================================
|
|
157
|
+
|
|
110
158
|
// ❌ Return validation errors if any exist
|
|
111
159
|
if (Object.keys(validationErrors).length > 0) {
|
|
112
160
|
logger.debug("🚨 Validation errors:", validationErrors, req);
|
|
@@ -163,7 +211,7 @@ export function govcyReviewPostHandler() {
|
|
|
163
211
|
// Add the reference number to the submission data
|
|
164
212
|
submissionData.referenceNumber = referenceNo;
|
|
165
213
|
logger.info("✅ Data submitted", siteId, referenceNo);
|
|
166
|
-
|
|
214
|
+
|
|
167
215
|
// Get the user email address
|
|
168
216
|
let emailAddress = "";
|
|
169
217
|
// if Update my details not provided the use user email
|
|
@@ -174,25 +222,33 @@ export function govcyReviewPostHandler() {
|
|
|
174
222
|
}
|
|
175
223
|
// add contact email to submission data
|
|
176
224
|
submissionData.contactEmailAddress = emailAddress;
|
|
177
|
-
|
|
225
|
+
|
|
178
226
|
// handle data layer submission
|
|
179
227
|
dataLayer.storeSiteSubmissionData(
|
|
180
228
|
req.session,
|
|
181
229
|
siteId,
|
|
182
230
|
submissionData);
|
|
183
|
-
|
|
231
|
+
|
|
184
232
|
//-- Send email to user
|
|
185
233
|
// Generate the email body
|
|
186
234
|
let emailBody = generateSubmitEmail(service, submissionData.printFriendlyData, referenceNo, req);
|
|
187
|
-
|
|
235
|
+
|
|
236
|
+
// ==========================================================
|
|
237
|
+
// Custom pages
|
|
238
|
+
// =========================================================
|
|
239
|
+
// 🆕 Reset per-session custom pages from the global app definition
|
|
240
|
+
const app = req.app; // ✅ Express automatically provides this
|
|
241
|
+
resetCustomPages(app, req.session, siteId);
|
|
242
|
+
// ==========================================================
|
|
243
|
+
|
|
188
244
|
// Send the email
|
|
189
245
|
sendEmail(
|
|
190
|
-
service.site.title[service.site.lang],
|
|
191
|
-
emailBody,
|
|
192
|
-
[emailAddress],
|
|
246
|
+
service.site.title[service.site.lang],
|
|
247
|
+
emailBody,
|
|
248
|
+
[emailAddress],
|
|
193
249
|
"eMail").catch(err => {
|
|
194
250
|
logger.error("Email sending failed (async):", err);
|
|
195
|
-
|
|
251
|
+
});
|
|
196
252
|
// --- End of email sending
|
|
197
253
|
|
|
198
254
|
logger.debug("🔄 Redirecting to success page:", req);
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import * as govcyResources from "../resources/govcyResources.mjs";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Defines custom pages for a given siteId and pageUrl.
|
|
5
|
+
* This function initializes the custom pages in the store and sets up the necessary data structure.
|
|
6
|
+
* @param {object} store The session store (usually req.app)
|
|
7
|
+
* @param {string} siteId The site identifier (e.g., `"cso"`)
|
|
8
|
+
* @param {string} pageUrl The URL of the custom page (e.g., `"/cso/custom"`)
|
|
9
|
+
* @param {string} pageTitle The title of the custom page (e.g., `{ en: "My custom section", el: "Προσαρμοσμένη ενότητα" }`)
|
|
10
|
+
* @param {string} insertAfterPageUrl The page URL to insert the custom page after (e.g., `"qualifications"`)
|
|
11
|
+
* @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 {object} [extraProps={}] Optional extra metadata (e.g., `{ nextPage: "confirmation", requiresOTP: true }`)
|
|
15
|
+
*/
|
|
16
|
+
export function defineCustomPages(
|
|
17
|
+
store,
|
|
18
|
+
siteId,
|
|
19
|
+
pageUrl,
|
|
20
|
+
pageTitle,
|
|
21
|
+
insertAfterPageUrl,
|
|
22
|
+
errors,
|
|
23
|
+
summaryElements,
|
|
24
|
+
summaryHtml = false,
|
|
25
|
+
extraProps = {}
|
|
26
|
+
) {
|
|
27
|
+
// Initialize custom pages in session if not already set
|
|
28
|
+
store.siteData ??= {};
|
|
29
|
+
store.siteData[siteId] ??= {};
|
|
30
|
+
store.siteData[siteId].customPagesDefinition ??= {}; // Initialize custom pages definition
|
|
31
|
+
store.siteData[siteId].customPagesDefinition[pageUrl] ??= {}; // Initialize specific page definition
|
|
32
|
+
|
|
33
|
+
// ✅ Normalize error objects: add pageUrl if missing
|
|
34
|
+
let normalizedErrors = [];
|
|
35
|
+
if (Array.isArray(errors)) {
|
|
36
|
+
normalizedErrors = errors.map(err => ({
|
|
37
|
+
...err,
|
|
38
|
+
// pageUrl replace first `/` with empty string to ensure it is relative
|
|
39
|
+
pageUrl: err.pageUrl || pageUrl.replace(/^\//, "") // ← ensure pageUrl is set
|
|
40
|
+
}));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Initialize the custom summary actions
|
|
44
|
+
let summaryActions = [];
|
|
45
|
+
|
|
46
|
+
// Base definition
|
|
47
|
+
const definition = {
|
|
48
|
+
pageTitle,
|
|
49
|
+
insertAfterPageUrl,
|
|
50
|
+
summaryElements: summaryElements || [],
|
|
51
|
+
errors: normalizedErrors,
|
|
52
|
+
summaryActions: summaryActions,
|
|
53
|
+
...(summaryHtml ? { summaryHtml } : {}),
|
|
54
|
+
...extraProps // ✅ Merge any additional developer-defined properties
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
// Construct default summaryActions
|
|
58
|
+
if (pageTitle && pageUrl) {
|
|
59
|
+
definition.summaryActions = [
|
|
60
|
+
{
|
|
61
|
+
text: govcyResources.staticResources.text.change,
|
|
62
|
+
classes: "govcy-d-print-none",
|
|
63
|
+
href: `${pageUrl}?route=review`,
|
|
64
|
+
visuallyHiddenText: pageTitle
|
|
65
|
+
}
|
|
66
|
+
];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Save definition
|
|
70
|
+
store.siteData[siteId].customPagesDefinition[pageUrl] = definition;
|
|
71
|
+
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Retrieves the custom page definition for a given siteId and pageUrl.
|
|
76
|
+
*
|
|
77
|
+
* @param {object} store The custom pages configuration store. Should be req.app
|
|
78
|
+
* @param {string} siteId The site id
|
|
79
|
+
* @param {string} pageUrl The URL of the custom page
|
|
80
|
+
* @returns {object} The custom page definition or an empty object if not found.
|
|
81
|
+
*/
|
|
82
|
+
export function getCustomPageDefinition(store, siteId, pageUrl) {
|
|
83
|
+
return store.siteData?.[siteId]?.customPagesDefinition?.[pageUrl] || {};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Resets the custom pages for a given siteId.
|
|
88
|
+
* This will deep copy the customPagesDefinition to customPages.
|
|
89
|
+
* This is useful for initializing or resetting the custom pages.
|
|
90
|
+
*
|
|
91
|
+
* @param {object} configStore The custom pages configuration store. Should be req.app
|
|
92
|
+
* @param {object} store The data layer store. Should be the req.session
|
|
93
|
+
* @param {string} siteId The site id
|
|
94
|
+
*/
|
|
95
|
+
export function resetCustomPages(configStore, store, siteId) {
|
|
96
|
+
if (!configStore?.siteData?.[siteId]?.customPagesDefinition) return;
|
|
97
|
+
// Reset custom pages for the given siteId based on the customPagesDefinition
|
|
98
|
+
store.siteData[siteId].customPages = JSON.parse(JSON.stringify(configStore.siteData[siteId].customPagesDefinition))
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ==========================================================
|
|
102
|
+
// 🧰 Custom Page Error Helpers
|
|
103
|
+
// ==========================================================
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Sets a custom property on a given custom page definition or instance.
|
|
107
|
+
*
|
|
108
|
+
* @param {object} store - The store containing siteData.
|
|
109
|
+
* @param {string} siteId - The site identifier.
|
|
110
|
+
* @param {string} pageUrl - The custom page URL.
|
|
111
|
+
* @param {string} property - The property name to set.
|
|
112
|
+
* @param {*} value - The value to assign.
|
|
113
|
+
* @param {boolean} [isDefinition=false] - Whether to modify the global definition instead of session data.
|
|
114
|
+
*/
|
|
115
|
+
export function setCustomPageProperty(store, siteId, pageUrl, property, value, isDefinition = false) {
|
|
116
|
+
const container = isDefinition
|
|
117
|
+
? store.siteData?.[siteId]?.customPagesDefinition
|
|
118
|
+
: store.siteData?.[siteId]?.customPages;
|
|
119
|
+
|
|
120
|
+
if (!container || !container[pageUrl]) {
|
|
121
|
+
console.warn(`⚠️ setCustomPageProperty: page '${pageUrl}' not found for site '${siteId}'`);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
container[pageUrl][property] = value;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Gets a custom property from a given custom page definition or instance.
|
|
130
|
+
*
|
|
131
|
+
* @param {object} store - The store containing siteData.
|
|
132
|
+
* @param {string} siteId - The site identifier.
|
|
133
|
+
* @param {string} pageUrl - The custom page URL.
|
|
134
|
+
* @param {string} property - The property name to get.
|
|
135
|
+
* @param {boolean} [isDefinition=false] - Whether to read from the global definition instead of session data.
|
|
136
|
+
* @returns {*} The value of the property, or undefined if not found.
|
|
137
|
+
*/
|
|
138
|
+
export function getCustomPageProperty(store, siteId, pageUrl, property, isDefinition = false) {
|
|
139
|
+
const container = isDefinition
|
|
140
|
+
? store.siteData?.[siteId]?.customPagesDefinition
|
|
141
|
+
: store.siteData?.[siteId]?.customPages;
|
|
142
|
+
|
|
143
|
+
return container?.[pageUrl]?.[property];
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Sets the data object for a given custom page.
|
|
149
|
+
* Overwrites the existing data (does not merge).
|
|
150
|
+
*
|
|
151
|
+
* @param {Object} store - The store containing siteData.
|
|
152
|
+
* @param {string} siteId - The site identifier.
|
|
153
|
+
* @param {string} pageUrl - The URL of the custom page.
|
|
154
|
+
* @param {Object} data - The data to store.
|
|
155
|
+
*/
|
|
156
|
+
export function setCustomPageData(store, siteId, pageUrl, data) {
|
|
157
|
+
|
|
158
|
+
const target = store.siteData?.[siteId]?.customPages?.[pageUrl];
|
|
159
|
+
if (!target) {
|
|
160
|
+
console.warn(`⚠️ setCustomPageData: page '${pageUrl}' not found for site '${siteId}'`);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
target.data = data; // overwrite existing data
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Sets the email array with dsf-email-templates objects for a given custom page.
|
|
169
|
+
* Overwrites the existing data (does not merge).
|
|
170
|
+
*
|
|
171
|
+
* @param {Object} store - The store containing siteData.
|
|
172
|
+
* @param {string} siteId - The site identifier.
|
|
173
|
+
* @param {string} pageUrl - The URL of the custom page.
|
|
174
|
+
* @param {Array<Object>} arrayOfEmailObjects - Array of dsf-email-templates objects.
|
|
175
|
+
*/
|
|
176
|
+
export function setCustomPageEmail(store, siteId, pageUrl, arrayOfEmailObjects) {
|
|
177
|
+
|
|
178
|
+
const target = store.siteData?.[siteId]?.customPages?.[pageUrl];
|
|
179
|
+
if (!target) {
|
|
180
|
+
console.warn(`⚠️ setCustomPageData: page '${pageUrl}' not found for site '${siteId}'`);
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (!Array.isArray(arrayOfEmailObjects)) {
|
|
185
|
+
console.warn(`⚠️ setCustomPageEmail: expected array for custom page '${normalizedPageUrl}', got`, typeof arrayOfEmailObjects);
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
target.email = arrayOfEmailObjects; // overwrite existing data
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Sets the summaryElements array for a given custom page.
|
|
194
|
+
* Replaces the existing summaryElements array.
|
|
195
|
+
*
|
|
196
|
+
* @param {Object} store - The store containing siteData.
|
|
197
|
+
* @param {string} siteId - The site identifier.
|
|
198
|
+
* @param {string} pageUrl - The URL of the custom page.
|
|
199
|
+
* @param {Array} summaryElements - Array of summary element definitions.
|
|
200
|
+
*/
|
|
201
|
+
export function setCustomPageSummaryElements(store, siteId, pageUrl, summaryElements) {
|
|
202
|
+
|
|
203
|
+
const target = store.siteData?.[siteId]?.customPages?.[pageUrl];
|
|
204
|
+
if (!target) {
|
|
205
|
+
console.warn(`⚠️ setCustomPageSummaryElements: page '${pageUrl}' not found for site '${siteId}'`);
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
target.summaryElements = Array.isArray(summaryElements) ? summaryElements : [];
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Clears all errors for a given custom page.
|
|
214
|
+
* Works on either customPages or customPagesDefinition.
|
|
215
|
+
*
|
|
216
|
+
* @param {Object} store - The store containing siteData.
|
|
217
|
+
* @param {string} siteId - The site identifier.
|
|
218
|
+
* @param {string} pageUrl - The URL of the custom page.
|
|
219
|
+
*/
|
|
220
|
+
export function clearCustomPageErrors(store, siteId, pageUrl) {
|
|
221
|
+
|
|
222
|
+
const data = store.siteData?.[siteId]?.customPages?.[pageUrl];
|
|
223
|
+
|
|
224
|
+
if (data) data.errors = [];
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Adds an error to a given custom page (definition or data).
|
|
229
|
+
* Auto-fills pageUrl and ensures array exists.
|
|
230
|
+
*
|
|
231
|
+
* @param {Object} store - The store containing siteData.
|
|
232
|
+
* @param {string} siteId - The site identifier.
|
|
233
|
+
* @param {string} pageUrl - The URL of the custom page.
|
|
234
|
+
* @param {string} errorId - Unique identifier for the error.
|
|
235
|
+
* @param {Object} errorTextObject - Multilingual Object containing localized error texts.
|
|
236
|
+
*/
|
|
237
|
+
export function addCustomPageError(store, siteId, pageUrl, errorId, errorTextObject) {
|
|
238
|
+
const normalizedPageUrl = pageUrl.replace(/^\/+/, "");
|
|
239
|
+
|
|
240
|
+
const target =
|
|
241
|
+
store.siteData?.[siteId]?.customPages?.[pageUrl]
|
|
242
|
+
|
|
243
|
+
if (!target) {
|
|
244
|
+
console.warn(`⚠️ addCustomPageError: page '${pageUrl}' not found for site '${siteId}'`);
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
target.errors ??= [];
|
|
249
|
+
|
|
250
|
+
// ✅ Prevent duplicate errors by ID
|
|
251
|
+
if (target.errors.some(err => err.id === errorId)) return;
|
|
252
|
+
|
|
253
|
+
// Normalize and push error
|
|
254
|
+
target.errors.push({
|
|
255
|
+
id: errorId,
|
|
256
|
+
text: errorTextObject,
|
|
257
|
+
pageUrl: normalizedPageUrl,
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
}
|
|
@@ -378,6 +378,23 @@ export function getSiteLoadData(store, siteId) {
|
|
|
378
378
|
return null;
|
|
379
379
|
}
|
|
380
380
|
|
|
381
|
+
/**
|
|
382
|
+
* Get the site's custom pages from the store for custom pages
|
|
383
|
+
*
|
|
384
|
+
* @param {object} store The session store
|
|
385
|
+
* @param {string} siteId |The site id
|
|
386
|
+
* @returns The site custom pages or null if none exist.
|
|
387
|
+
*/
|
|
388
|
+
export function getSiteCustomPages(store, siteId) {
|
|
389
|
+
const customPages = store?.siteData?.[siteId]?.customPages || {};
|
|
390
|
+
|
|
391
|
+
if (customPages ) {
|
|
392
|
+
return customPages ;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
return null;
|
|
396
|
+
}
|
|
397
|
+
|
|
381
398
|
/**
|
|
382
399
|
* Get the site's reference number from load data from the store
|
|
383
400
|
*
|
|
@@ -50,13 +50,13 @@ export function prepareSubmissionData(req, siteId, service) {
|
|
|
50
50
|
if (page.updateMyDetails) {
|
|
51
51
|
logger.debug("Preparing submission data for UpdateMyDetails page", { siteId, pageUrl });
|
|
52
52
|
// Build the manual UMD page template
|
|
53
|
-
const umdTemplate
|
|
54
|
-
|
|
53
|
+
const umdTemplate = createUmdManualPageTemplate(siteId, service.site.lang, page, req);
|
|
54
|
+
|
|
55
55
|
// Extract the form element
|
|
56
|
-
formElement = umdTemplate
|
|
56
|
+
formElement = umdTemplate.sections
|
|
57
57
|
.flatMap(section => section.elements)
|
|
58
58
|
.find(el => el.element === "form");
|
|
59
|
-
|
|
59
|
+
|
|
60
60
|
if (!formElement) {
|
|
61
61
|
logger.error("🚨 UMD form element not found during prepareSubmissionData", { siteId, pageUrl });
|
|
62
62
|
return handleMiddlewareError("🚨 UMD form element not found during prepareSubmissionData", 500, next);
|
|
@@ -190,11 +190,51 @@ export function prepareSubmissionData(req, siteId, service) {
|
|
|
190
190
|
|
|
191
191
|
// Get the renderer data from the session store
|
|
192
192
|
const reviewSummaryList = generateReviewSummary(printFriendlyData, req, siteId, false);
|
|
193
|
+
|
|
194
|
+
// ==========================================================
|
|
195
|
+
// Custom pages
|
|
196
|
+
// ==========================================================
|
|
197
|
+
const customPages = dataLayer.getSiteCustomPages(req.session, siteId) || {};
|
|
198
|
+
|
|
199
|
+
// Convert submissionData keys into ordered array for splicing
|
|
200
|
+
const orderedEntries = Object.entries(submissionData);
|
|
201
|
+
|
|
202
|
+
// Loop through each custom page
|
|
203
|
+
for (const [customKey, customPage] of Object.entries(customPages)) {
|
|
204
|
+
const pageData = customPage?.data || "";
|
|
205
|
+
const insertAfter = customPage?.insertAfterPageUrl; // normalize
|
|
206
|
+
|
|
207
|
+
// Build a new entry for this custom page
|
|
208
|
+
const customEntry = [customKey, pageData];
|
|
209
|
+
|
|
210
|
+
if (insertAfter) {
|
|
211
|
+
// Find the index of the page to insert after
|
|
212
|
+
const idx = orderedEntries.findIndex(([pageUrl]) => pageUrl === insertAfter);
|
|
213
|
+
if (idx >= 0) {
|
|
214
|
+
// Insert right after the matched page
|
|
215
|
+
orderedEntries.splice(idx + 1, 0, customEntry);
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Fallback: append to the top
|
|
221
|
+
orderedEntries.unshift(customEntry);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Convert back to an object in the new order
|
|
225
|
+
const orderedSubmissionData = Object.fromEntries(orderedEntries);
|
|
226
|
+
|
|
227
|
+
logger.info("Submission Data with custom pages merged");
|
|
228
|
+
|
|
229
|
+
// ==========================================================
|
|
230
|
+
// END custom page merge
|
|
231
|
+
// ==========================================================
|
|
232
|
+
|
|
193
233
|
// Prepare the submission data object
|
|
194
234
|
return {
|
|
195
235
|
submissionUsername: dataLayer.getUser(req.session).name,
|
|
196
236
|
submissionEmail: dataLayer.getUser(req.session).email,
|
|
197
|
-
submissionData:
|
|
237
|
+
submissionData: orderedSubmissionData, // Raw data as submitted by the user in each page
|
|
198
238
|
submissionDataVersion: service.site?.submissionDataVersion || service.site?.submission_data_version || "", // The submission data version
|
|
199
239
|
printFriendlyData: printFriendlyData, // Print-friendly data
|
|
200
240
|
rendererData: reviewSummaryList, // Renderer data of the summary list
|
|
@@ -259,12 +299,12 @@ export function preparePrintFriendlyData(req, siteId, service) {
|
|
|
259
299
|
|
|
260
300
|
let pageTemplate = page.pageTemplate;
|
|
261
301
|
let pageTitle = page.pageData.title || {};
|
|
262
|
-
|
|
302
|
+
|
|
263
303
|
|
|
264
304
|
// ----- MultipleThings hub handling
|
|
265
305
|
if (page.updateMyDetails) {
|
|
266
306
|
// create the page template
|
|
267
|
-
pageTemplate = createUmdManualPageTemplate(siteId, service.site.lang, page, req
|
|
307
|
+
pageTemplate = createUmdManualPageTemplate(siteId, service.site.lang, page, req);
|
|
268
308
|
// set the page title
|
|
269
309
|
pageTitle = govcyResources.staticResources.text.updateMyDetailsTitle;
|
|
270
310
|
}
|
|
@@ -642,6 +682,7 @@ export function generateReviewSummary(submissionData, req, siteId, showChangeLin
|
|
|
642
682
|
|
|
643
683
|
// Add inner summary list to the main summary list
|
|
644
684
|
let outerSummaryList = {
|
|
685
|
+
"pageUrl": pageUrl, // add pageUrl for change link
|
|
645
686
|
"key": pageTitle,
|
|
646
687
|
"value": [summaryListInner],
|
|
647
688
|
"actions": [ //add change link
|
|
@@ -664,6 +705,76 @@ export function generateReviewSummary(submissionData, req, siteId, showChangeLin
|
|
|
664
705
|
|
|
665
706
|
}
|
|
666
707
|
|
|
708
|
+
// ==========================================================
|
|
709
|
+
// CustomPages
|
|
710
|
+
// ==========================================================
|
|
711
|
+
// Custom summaries are stored under:
|
|
712
|
+
// session.siteData[siteId].customPages
|
|
713
|
+
// Each entry may include:
|
|
714
|
+
// insertAfter: "pageUrl"
|
|
715
|
+
// summary: array of summaryList items (same shape as generateReviewSummary output)
|
|
716
|
+
// OR
|
|
717
|
+
// html: pre-rendered multilingual HTML block
|
|
718
|
+
// ==========================================================
|
|
719
|
+
|
|
720
|
+
const customPages = dataLayer.getSiteCustomPages(req.session, siteId);
|
|
721
|
+
|
|
722
|
+
for (const [key, block] of Object.entries(customPages)) { //
|
|
723
|
+
// Build the structure the same way as a normal section
|
|
724
|
+
let customEntry;
|
|
725
|
+
|
|
726
|
+
let customValue = [];
|
|
727
|
+
|
|
728
|
+
if (block.summaryHtml) {
|
|
729
|
+
// Direct HTML block
|
|
730
|
+
customValue = [
|
|
731
|
+
{
|
|
732
|
+
element: "htmlElement",
|
|
733
|
+
params: { text: block.summaryHtml }
|
|
734
|
+
}
|
|
735
|
+
];
|
|
736
|
+
} else if (block.summaryElements) {
|
|
737
|
+
// Already a ready-made summaryList object
|
|
738
|
+
customValue = [
|
|
739
|
+
{
|
|
740
|
+
element: "summaryList",
|
|
741
|
+
params: { items: block.summaryElements }
|
|
742
|
+
}
|
|
743
|
+
];
|
|
744
|
+
} else {
|
|
745
|
+
// Fallback empty block
|
|
746
|
+
continue; // skip this block if no valid content
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
customEntry = {
|
|
750
|
+
key: block.pageTitle || { en: key, el: key },
|
|
751
|
+
value: customValue,
|
|
752
|
+
actions: block.summaryActions || [] // Optional actions, e.g. change links
|
|
753
|
+
};
|
|
754
|
+
|
|
755
|
+
// If showChangeLinks, remove the actions key
|
|
756
|
+
if (!showChangeLinks) {
|
|
757
|
+
delete customEntry.actions;
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
// Insert custom summary section after given page
|
|
761
|
+
if (block.insertAfterPageUrl) {
|
|
762
|
+
const idx = summaryList.params.items.findIndex(
|
|
763
|
+
(p) => p.pageUrl === block.insertAfterPageUrl);
|
|
764
|
+
if (idx >= 0) {
|
|
765
|
+
summaryList.params.items.splice(idx + 1, 0, customEntry);
|
|
766
|
+
} else {
|
|
767
|
+
summaryList.params.items.push(customEntry); // fallback append
|
|
768
|
+
}
|
|
769
|
+
} else {
|
|
770
|
+
summaryList.params.items.unshift(customEntry); // fallback append
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
// ==========================================================
|
|
775
|
+
// END customPages merge
|
|
776
|
+
// ==========================================================
|
|
777
|
+
|
|
667
778
|
return summaryList;
|
|
668
779
|
}
|
|
669
780
|
|
|
@@ -680,6 +791,14 @@ export function generateReviewSummary(submissionData, req, siteId, showChangeLin
|
|
|
680
791
|
*/
|
|
681
792
|
export function generateSubmitEmail(service, submissionData, submissionId, req) {
|
|
682
793
|
let body = [];
|
|
794
|
+
// ==========================================================
|
|
795
|
+
// Custom page
|
|
796
|
+
// ==========================================================
|
|
797
|
+
const customPages = dataLayer.getSiteCustomPages(req.session, service.site.id) || {};
|
|
798
|
+
// Build a lookup for custom pages by insertAfterPageUrl
|
|
799
|
+
const customPageEntries = Object.entries(customPages);
|
|
800
|
+
let pagesInBody = [];
|
|
801
|
+
// ===========================================================
|
|
683
802
|
|
|
684
803
|
//check if there is submission Id
|
|
685
804
|
if (submissionId) {
|
|
@@ -772,8 +891,57 @@ export function generateSubmitEmail(service, submissionData, submissionId, req)
|
|
|
772
891
|
});
|
|
773
892
|
}
|
|
774
893
|
|
|
894
|
+
// ==========================================================
|
|
895
|
+
// Custom page
|
|
896
|
+
// ==========================================================
|
|
897
|
+
// 🆕 Check for custom pages that should appear after this one
|
|
898
|
+
for (const [customKey, customPage] of customPageEntries) {
|
|
899
|
+
const insertAfter = customPage.insertAfterPageUrl;
|
|
900
|
+
if (insertAfter && insertAfter === pageUrl && Array.isArray(customPage.email)) {
|
|
901
|
+
pagesInBody.push(customPage.pageUrl); // Track custom page key for later
|
|
902
|
+
// Add data title to the body
|
|
903
|
+
body.push(
|
|
904
|
+
{
|
|
905
|
+
component: "bodyHeading",
|
|
906
|
+
params: { "headingLevel": 2 },
|
|
907
|
+
body: govcyResources.getLocalizeContent(customPage.pageTitle, req.globalLang)
|
|
908
|
+
}
|
|
909
|
+
)
|
|
910
|
+
logger.debug(`📧 Inserting custom page email '${customKey}' after '${pageUrl}'`);
|
|
911
|
+
body.push(...customPage.email);
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
// ==========================================================
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
// ==========================================================
|
|
919
|
+
// Custom pages - Handle leftover custom pages not yet inserted
|
|
920
|
+
// ==========================================================
|
|
921
|
+
for (const [customKey, customPage] of customPageEntries) {
|
|
922
|
+
// check if this custom page was already inserted
|
|
923
|
+
const wasInserted = pagesInBody.includes(customKey);
|
|
924
|
+
// check if it has email content
|
|
925
|
+
const hasEmail = Array.isArray(customPage.email);
|
|
926
|
+
// get its title
|
|
927
|
+
const pageTitle = customPage.pageTitle;
|
|
928
|
+
|
|
929
|
+
// If not inserted and has email content, prepend it to the body
|
|
930
|
+
if (!wasInserted && hasEmail) {
|
|
931
|
+
// Prepend its email content
|
|
932
|
+
body.unshift(...customPage.email);
|
|
933
|
+
// Add a heading for visibility
|
|
934
|
+
body.unshift({
|
|
935
|
+
component: "bodyHeading",
|
|
936
|
+
params: { headingLevel: 2 },
|
|
937
|
+
body: govcyResources.getLocalizeContent(pageTitle, req.globalLang)
|
|
938
|
+
});
|
|
775
939
|
|
|
940
|
+
logger.debug(`📧 Adding leftover custom page '${customKey}' (no insertAfter match)`);
|
|
941
|
+
}
|
|
776
942
|
}
|
|
943
|
+
// ==========================================================
|
|
944
|
+
|
|
777
945
|
|
|
778
946
|
let emailObject = govcyResources.getEmailObject(
|
|
779
947
|
service.site.title,
|
|
@@ -790,86 +958,3 @@ export function generateSubmitEmail(service, submissionData, submissionId, req)
|
|
|
790
958
|
return emailRenderer.renderFromJson(emailObject);
|
|
791
959
|
}
|
|
792
960
|
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
/*
|
|
796
|
-
{
|
|
797
|
-
"bank-details": {
|
|
798
|
-
"formData": {
|
|
799
|
-
"AccountName": "asd",
|
|
800
|
-
"Iban": "CY12 0020 0123 0000 0001 2345 6789",
|
|
801
|
-
"Swift": "BANKCY2NXXX",
|
|
802
|
-
"_csrf": "sjknv79rxjgv0uggo0d5312vzgz37jsh"
|
|
803
|
-
}
|
|
804
|
-
},
|
|
805
|
-
"answer-bank-boc": {
|
|
806
|
-
"formData": {
|
|
807
|
-
"Objection": "Object",
|
|
808
|
-
"country": "Azerbaijan",
|
|
809
|
-
"ObjectionReason": "ObjectionReasonCode1",
|
|
810
|
-
"ObjectionExplanation": "asdsa",
|
|
811
|
-
"DepositsBOCAttachment": "",
|
|
812
|
-
"_csrf": "sjknv79rxjgv0uggo0d5312vzgz37jsh"
|
|
813
|
-
}
|
|
814
|
-
},
|
|
815
|
-
"bank-settlement": {
|
|
816
|
-
"formData": {
|
|
817
|
-
"ReceiveSettlementExplanation": "",
|
|
818
|
-
"ReceiveSettlementDate_day": "",
|
|
819
|
-
"ReceiveSettlementDate_month": "",
|
|
820
|
-
"ReceiveSettlementDate_year": "",
|
|
821
|
-
"ReceiveSettlement": "no",
|
|
822
|
-
"_csrf": "sjknv79rxjgv0uggo0d5312vzgz37jsh"
|
|
823
|
-
}
|
|
824
|
-
}
|
|
825
|
-
}
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
[
|
|
830
|
-
{
|
|
831
|
-
pageUrl: "personal-details",
|
|
832
|
-
pageTitle: { en: "Personal data", el: "Προσωπικά στοιχεία" }, // from pageData.title in correct language
|
|
833
|
-
fields: [
|
|
834
|
-
[
|
|
835
|
-
{
|
|
836
|
-
id: "firstName",
|
|
837
|
-
label: { en: "First Name", el: "Όνομα" },
|
|
838
|
-
value: "John", // The actual user input value
|
|
839
|
-
valueLabel: { en: "John", el: "John" } // Same label as the value for text inputs
|
|
840
|
-
},
|
|
841
|
-
{
|
|
842
|
-
id: "lastName",
|
|
843
|
-
label: { en: "Last Name", el: "Επίθετο" },
|
|
844
|
-
value: "Doe", // The actual user input value
|
|
845
|
-
valueLabel: { en: "Doe", el: "Doe" } // Same label as the value for text inputs
|
|
846
|
-
},
|
|
847
|
-
{
|
|
848
|
-
id: "gender",
|
|
849
|
-
label: { en: "Gender", el: "Φύλο" },
|
|
850
|
-
value: "m", // The actual value ("male")
|
|
851
|
-
valueLabel: { en: "Male", el: "Άντρας" } // The corresponding label for "male"
|
|
852
|
-
},
|
|
853
|
-
{
|
|
854
|
-
id: "languages",
|
|
855
|
-
label: { en: "Languages", el: "Γλώσσες" },
|
|
856
|
-
value: ["en", "el"], // The selected values ["en", "el"]
|
|
857
|
-
valueLabel: [
|
|
858
|
-
{ en: "English", el: "Αγγλικά" }, // Labels corresponding to "en" and "el"
|
|
859
|
-
{ en: "Greek", el: "Ελληνικά" }
|
|
860
|
-
]
|
|
861
|
-
},
|
|
862
|
-
{
|
|
863
|
-
id: "birthDate",
|
|
864
|
-
label: { en: "Birth Date", el: "Ημερομηνία Γέννησης" },
|
|
865
|
-
value: "1990-01-13", // The actual value based on user input
|
|
866
|
-
valueLabel: "13/1/1990" // Date inputs label will be conveted to D/M/YYYY
|
|
867
|
-
}
|
|
868
|
-
]
|
|
869
|
-
},
|
|
870
|
-
...
|
|
871
|
-
]
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
*/
|