@gov-cy/govcy-express-services 1.3.0-alpha → 1.3.0-alpha.2

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 CHANGED
@@ -31,6 +31,11 @@ The APIs used for submission, temporary save and file uploads are not part of th
31
31
  - [📦 Full installation guide](#-full-installation-guide)
32
32
  - [🛠️ Usage](#%EF%B8%8F-usage)
33
33
  - [🧩 Dynamic services](#-dynamic-services)
34
+ - [Pages](#pages)
35
+ - [Form vs static pages](#form-vs-static-pages)
36
+ - [Multiple things pages (repeating group of inputs)](#multiple-things-pages-repeating-group-of-inputs)
37
+ - [Review page](#review-page)
38
+ - [Success page](#success-page)
34
39
  - [🛡️ Site eligibility checks](#%EF%B8%8F-site-eligibility-checks)
35
40
  - [📤 Site submissions](#-site-submissions)
36
41
  - [✅ Input validations](#-input-validations)
@@ -854,7 +859,7 @@ Here's an example of a page defined in the JSON file:
854
859
 
855
860
  The above `page` JSON generates a page that looks like the following screenshot:
856
861
 
857
- ![Screenshot of sample page](express-page.png)
862
+ ![Screenshot of sample page](docs/img/express-page.png)
858
863
 
859
864
  The JSON structure is based on the [govcy-frontend-renderer's JSON template](https://github.com/gov-cy/govcy-frontend-renderer/blob/main/README.md#json-template-example).
860
865
 
@@ -871,7 +876,7 @@ Lets break down the JSON config for this page:
871
876
  - `sections` is an array of sections, which is an array of elements. Sections allowed: `beforeMain`, `main`, `afterMain`.
872
877
  - `elements` is an array of elements for the said section. Seem more details on the [govcy-frontend-renderer's design elements documentation](https://github.com/gov-cy/govcy-frontend-renderer/blob/main/DESIGN_ELEMENTS.md).
873
878
 
874
- **Forms vs static content**
879
+ #### Form vs static pages
875
880
 
876
881
  - If the `pageTemplate` includes a `form` element in the `main` section and `button` element, the system will treat it as form and will:
877
882
  - Perform the eligibility checks
@@ -904,6 +909,226 @@ The [start page](https://gov-cy.github.io/govcy-design-system-docs/patterns/serv
904
909
  - Check out the [govcy-frontend-renderer's design elements](https://github.com/gov-cy/govcy-frontend-renderer/blob/main/DESIGN_ELEMENTS.md) for more details on the supported elements and their parameters.
905
910
  - Check out the [input validations section](#-input-validations) for more details on how to add validations to the JSON file.
906
911
 
912
+ #### Multiple things pages (repeating group of inputs)
913
+ ![Multiple things pages](docs/img/express-mt.png)
914
+
915
+ Some services need to collect **multiple entries of the same structure**, for example, academic qualifications, addresses, or dependents. The framework supports this through the `multipleThings` block in a page config. It uses the same input method of a normal form page, but the user can add more than one entries.
916
+
917
+ When enabled, the framework automatically generates a **hub page** when the user visits the:
918
+
919
+ ```ruby
920
+ /:siteId/:pageUrl
921
+ ```
922
+
923
+ This hub page allows users to:
924
+ - 📋 **View the list** of entries (hub page)
925
+ - ➕ **Add** a new entry
926
+ - ✏️ **Change** an existing entry
927
+ - ❌ **Remove** an existing entry
928
+ - ✅ **Continue** when they are finished
929
+
930
+ **Multiple things - example JSON config**
931
+ Here’s how to define a page that collects multiple academic qualifications:
932
+
933
+ ```json
934
+ {
935
+ "pageData": {
936
+ "url": "qualifications", // Page URL
937
+ "title": { // Page title of the input pages (add, edit)
938
+ "el": "Ακαδημαϊκά και επαγγελματικά προσόντα",
939
+ "en": "Academic and professional qualifications",
940
+ "tr": ""
941
+ },
942
+ "layout": "layouts/govcyBase.njk", // Page layout for all pages (add, edit, hub page)
943
+ "mainLayout": "two-third", // Page main layout for all pages (add, edit, hub page)
944
+ "nextPage": "memberships" // The next page's URL
945
+ },
946
+ "pageTemplate" : { // Page template for the input pages (add, edit)
947
+ ...
948
+ },
949
+ "multipleThings": { // Multiple things configuration
950
+ "min": 1, // the minimum number of entries
951
+ "max": 5, // the maximum number of entries
952
+ "dedupe": true, // whether to check for duplicates
953
+ "itemTitleTemplate": "{{title | trim}} - {{year | trim}}", // the title template for each entry
954
+ "listPage": { // the hub page
955
+ "title": { // the hub page title
956
+ "el": "Ακαδημαϊκά προσόντα",
957
+ "en": "Academic details"
958
+ },
959
+ "topElements": [ // the hub page top elements
960
+ {
961
+ "element": "progressList",
962
+ "params": {
963
+ "id": "progress",
964
+ "current": "2",
965
+ "total": "6",
966
+ "showSteps": true
967
+ }
968
+ },
969
+ {
970
+ "element": "textElement",
971
+ "params": {
972
+ "id": "header",
973
+ "type": "h1",
974
+ "text": {
975
+ "el": "Ποια είναι τα προσόντα σας;",
976
+ "en": "What are your qualifications?",
977
+ "tr": ""
978
+ }
979
+ }
980
+ },
981
+ {
982
+ "element": "textElement",
983
+ "params": {
984
+ "id": "instructions",
985
+ "type": "p",
986
+ "text": {
987
+ "el": "Προσθέστε τουλάχιστον ένα τα ακαδημαϊκό ή επαγγελματικό προσόν για να συνεχίσετε. Μπορείτε να προσθέσετε μέχρι 5.",
988
+ "en": "Add at least one academic or professional qualifications to proceed. You can up to 5."
989
+ }
990
+ }
991
+ }
992
+ ],
993
+ "emptyState": { // the hub page empty state
994
+ "en": "No qualifications added yet.",
995
+ "el": "Δεν έχετε προσθέσει ακόμη προσόντα."
996
+ },
997
+ "addButtonText": { // the hub page add button text
998
+ "en": "➕ Add qualifications",
999
+ "el": "➕ Προσθήκη προσόντος"
1000
+ },
1001
+ "addButtonPlacement": "top", // the hub page add button placement
1002
+ "continueButtonText": { // the hub page continue button text
1003
+ "en": "Save and continue",
1004
+ "el": "Αποθήκευση και συνέχεια"
1005
+ },
1006
+ "hasBackLink": true // whether the hub page has a back link
1007
+ }
1008
+ }
1009
+ }
1010
+ ```
1011
+
1012
+ Lets break down the JSON config for multiple things:
1013
+ - **multipleThings** are the page's definition for repeated group of inputs
1014
+ - `multipleThings.min` : The minimum items rule
1015
+ - `multipleThings.max` : The maximum items rule
1016
+ - `multipleThings.dedupe`: When `true` prevents duplicates, using the `itemTitleTemplate` template. Optional with default value `false`
1017
+ - `multipleThings.itemTitleTemplate`: The template (in Nunjucks form) used to display the items' on a list in the hub, review, success pages and email. Also used by the `dedupe` to compare for duplicates
1018
+ - `multipleThings.listPage`: The definition the list (hub) page that shows the list of items, with the actions (add, edit, delete, continue)
1019
+ - `multipleThings.listPage.title`: The title used in the hub page's meta data
1020
+ - `multipleThings.listPage.topElements`: The elements to be displayed on the top of the page (more details on the [govcy-frontend-renderer's design elements documentation](https://github.com/gov-cy/govcy-frontend-renderer/blob/main/DESIGN_ELEMENTS.md)):
1021
+ - `multipleThings.listPage.emptyState`: The message displayed when the count of items == 0. Optional, when not defined generic text is shown
1022
+ - `multipleThings.listPage.addButtonText`: The text displayed on the add link. Optional, when not defined generic text is shown
1023
+ - `multipleThings.listPage.addButtonPlacement`: Where to show the add link. Can be `top`, `bottom` or `both`. Optional, default is `bottom`
1024
+ - `multipleThings.listPage.continueButtonText`: The text displayed on the continue button text. Optional, default is `Continue`, `Συνέχεια`
1025
+ - `multipleThings.listPage.hasBackLink`: When `true` shows the standard back button. Optional, default is `true
1026
+
1027
+
1028
+ **Multiple things - How it works**
1029
+ With multiple things pages, the system collects multiple set of the same type of data. The set of data are collected by a single page (similar to a normal page). The multiple things consist of 4 type of pages, the `hub`, `add`, `edit` and `delete` pages
1030
+
1031
+ **Hub page**: When the user navigates through the service at `/:siteId/:pageUrl` (either coming from _review_ or _linear_ flow), the system shows the hub page. The hub page in general shows the list of entries, add links, continue button and validates the input (see below more details)
1032
+
1033
+ `Hub page add links`: If the user has not reached the maximum number of allowed entries, the system shows add links on the hub page. There is an option for a custom add link text with `multipleThings.listPage.addButtonText`. There is an option for `top`, `bottom` or `both` placement with `multipleThings.listPage.addButtonPlacement`
1034
+
1035
+ ![Multiple things hub - add links](docs/img/express-mt-hub-add.png)
1036
+
1037
+ `Hub empty state`: When no data are yet entered the hub shows an empty state message with an add link. There is an option for a custom empty state
1038
+
1039
+ ![Multiple things hub - empty state](docs/img/express-mt-hub-empty.png)
1040
+
1041
+ `Hub list state`: When data exist, they are shown as a list with `change` and `delete` links. The list is created based on an the `multipleThings.itemTitleTemplate`, for example ` {{institution}} - {{title}} - {{year}}`
1042
+
1043
+ ![Multiple things hub - list](docs/img/express-mt-hub-list.png)
1044
+
1045
+ `Hub max state`: If the data entries reached the maximum limit, the `add links` are removed and a `max limit reached` message is shown.
1046
+
1047
+ ![Multiple things hub - max](docs/img/express-mt-hub-max.png)
1048
+
1049
+ `Hub page continue`: on Continue the system continues to the next page defined in the `pageData.nextPage`. If user came from the review page, it returns to the review.
1050
+
1051
+ `Hub page validations`: The hub page validates the following when the continue button is pressed.
1052
+ - `Minimum entries` have been entered. This is based on the `multipleThings.min`.
1053
+ - `Maximum limit` has not been exceeded.. This is based on the `multipleThings.max`.
1054
+ - `All entries validations pass`. This is based on the `pageTemplate` element validations.
1055
+
1056
+ ![Multiple things hub - validations](docs/img/express-mt-hub-validagion.png)
1057
+
1058
+ **Add pages**
1059
+
1060
+ ![Multiple things add](docs/img/express-mt-add.png)
1061
+
1062
+ - Accessible through the add link or through the `/:siteId/:pageUrl/multiple/add` route
1063
+ - User is shown the item page (e.g. `academic-details`)
1064
+ - Files' data are stored in a **draft** until they click `Continue`
1065
+ - On `Continue`, the data and draft is pushed into the list
1066
+ - Behaves like a normal form page
1067
+ - On `Continue` if there are no validation errors, the user is navigated back to the hub and the added data is displayed on the list. If the user’s journey started from the review page, after clicking continue on the hub, it goes back to the review.
1068
+ - It performs all configured input validations defined PLUS
1069
+ - `Maximum limit` has not been exceeded.
1070
+ - `Dedupe validation`. If defined in the configuration the service checks if there is an identical entry. The check is made based on the item title template
1071
+
1072
+ **Edit pages**
1073
+
1074
+ ![Multiple things edit](docs/img/express-mt-hub-edit.png)
1075
+
1076
+ - Accessible through the change link or through the `/:siteId/:pageUrl/multiple/edit/:index` route
1077
+ - User selects an existing item from the hub page
1078
+ - The item page loads pre-filled data
1079
+ - Behaves like a normal form page
1080
+ - On `Continue` if there are no validation errors, the entry is updated and the user is navigated back to the hub and the updated data is displayed on the list. If the user’s journey started from the review page, after clicking continue on the hub, it goes back to the review.
1081
+ - It performs all configured input validations defined PLUS
1082
+ - `Dedupe validation`. If defined in the configuration the service checks if there is an identical entry. The check is made based on the item title template
1083
+
1084
+ **Delete pages**
1085
+
1086
+ ![Multiple things delete](docs/img/express-mt-delete.png)
1087
+
1088
+ - Accessible through the remove link or through the `/:siteId/:pageUrl/multiple/delete/:index` route
1089
+ - A confirmation page is shown
1090
+ - On _Yes_, the item is removed from the list
1091
+
1092
+ **Review & Success pages**
1093
+
1094
+ ![Multiple things review](docs/img/express-mt-review.png)
1095
+
1096
+ - Each item is displayed in the summary list, grouped under the hub’s title
1097
+
1098
+ **Multiple things - data storage**
1099
+
1100
+ Form data for a `multipleThings` page is stored as an **array** in the session data layer:
1101
+ ```json
1102
+ {
1103
+ "academic-details": {
1104
+ "formData": [
1105
+ {
1106
+ "title": "BSc Computer Science",
1107
+ "academicFile": {
1108
+ "fileId": "12345",
1109
+ "sha256": "abcdef..."
1110
+ }
1111
+ },
1112
+ {
1113
+ "title": "MSc Information Systems",
1114
+ "academicFile": {
1115
+ "fileId": "67890",
1116
+ "sha256": "ghijkl..."
1117
+ }
1118
+ }
1119
+ ]
1120
+ }
1121
+ }
1122
+ ```
1123
+
1124
+ **Notes on multiple things**
1125
+
1126
+ - Each `multipleThings` page must define its own item page (with fields and validations), used for the `add` add `edit` routes.
1127
+ - The hub page is generated automatically. It uses the `multipleThings.listPage` for the UI
1128
+ - Empty state is handled automatically.
1129
+ - `multipleDraft` is used internally to store file's data while the user is adding a new item.
1130
+
1131
+
907
1132
  #### Review page
908
1133
 
909
1134
  The `review` page is automatically generated by the project and includes the following sections:
@@ -914,7 +1139,7 @@ The `review` page is automatically generated by the project and includes the fol
914
1139
 
915
1140
  Here's an example screenshot of review page
916
1141
 
917
- ![Screenshot of review page](express-review.png)
1142
+ ![Screenshot of review page](docs/img/express-review.png)
918
1143
 
919
1144
  When the user clicks a change link, the user is redirected to the corresponding page in the service. After the user clicks on `continue` button the user is redirected back to the `review` page.
920
1145
 
@@ -930,7 +1155,7 @@ The `success` page is automatically generated by the project, is accessible only
930
1155
 
931
1156
  Here's an example screenshot of success page
932
1157
 
933
- ![Screenshot of success page](express-success.png)
1158
+ ![Screenshot of success page](docs/img/express-success.png)
934
1159
 
935
1160
  ### 🛡️ Site eligibility checks
936
1161
 
@@ -1101,6 +1326,8 @@ HTTP/1.1 200 OK
1101
1326
  - If no `eligibilityAPIEndpoints` are configured, the system will not check for service eligibility for the specific site.
1102
1327
  - The response is normalized to always use PascalCase keys (`Succeeded`, `ErrorCode`, etc.), regardless of the backend’s casing.
1103
1328
  - If `Succeeded` is false, the system will look up the `ErrorCode` in your config to determine which error page to show.
1329
+ - For standard eligibility checks see:
1330
+ - [Elegibility with Civil Registry](docs/Eligibility-civil-registry.md)
1104
1331
 
1105
1332
  **Caching**
1106
1333
  - The response from each eligibility endpoint is cached in the session for the number of minutes specified by `cashingTimeoutMinutes`.
@@ -1321,378 +1548,30 @@ The data is collected from the form elements and the data layer and are sent via
1321
1548
  "date_on_contract": "date_other",
1322
1549
  "date_contract": "16/04/2025",
1323
1550
  "reason": "24324dssf"
1324
- }
1551
+ },
1552
+ "academic-details": [ // Multiple things page
1553
+ {
1554
+ "title": "BSc Computer Science",
1555
+ "academicFile": {
1556
+ "fileId": "12345",
1557
+ "sha256": "abcdef..."
1558
+ }
1559
+ },
1560
+ {
1561
+ "title": "MSc Information Systems",
1562
+ "academicFile": {
1563
+ "fileId": "67890",
1564
+ "sha256": "ghijkl..."
1565
+ }
1566
+ }
1567
+ ]
1325
1568
  },
1326
1569
  "submissionDataVersion": "1", // Submission data version
1327
1570
  "rendererData": { // Summary list renderer data ready for rendering . Object, will be stringified
1328
- "element": "summaryList",
1329
- "params": {
1330
- "items": [
1331
- {
1332
- "key": {
1333
- "el": "Στοιχεία του εκπαιδευτικού",
1334
- "en": "Educator's details",
1335
- "tr": ""
1336
- },
1337
- "value": [
1338
- {
1339
- "element": "summaryList",
1340
- "params": {
1341
- "items": [
1342
- {
1343
- "key": {
1344
- "el": "Ταυτοποίηση",
1345
- "en": "Identification"
1346
- },
1347
- "value": [
1348
- {
1349
- "element": "textElement",
1350
- "params": {
1351
- "text": {
1352
- "en": "Ταυτότητα, ARC",
1353
- "el": "Ταυτότητα, ARC",
1354
- "tr": "Ταυτότητα, ARC"
1355
- },
1356
- "type": "span"
1357
- }
1358
- }
1359
- ]
1360
- },
1361
- {
1362
- "key": {
1363
- "el": "Εισαγάγετε αριθμό ταυτότητας",
1364
- "en": "Enter ID number"
1365
- },
1366
- "value": [
1367
- {
1368
- "element": "textElement",
1369
- "params": {
1370
- "text": {
1371
- "en": "121212",
1372
- "el": "121212",
1373
- "tr": "121212"
1374
- },
1375
- "type": "span"
1376
- }
1377
- }
1378
- ]
1379
- },
1380
- {
1381
- "key": {
1382
- "el": "Αριθμός κοινωνικών ασφαλίσεων",
1383
- "en": "Social Insurance Number"
1384
- },
1385
- "value": [
1386
- {
1387
- "element": "textElement",
1388
- "params": {
1389
- "text": {
1390
- "en": "112121",
1391
- "el": "112121",
1392
- "tr": "112121"
1393
- },
1394
- "type": "span"
1395
- }
1396
- }
1397
- ]
1398
- }
1399
- ]
1400
- }
1401
- }
1402
- ]
1403
- },
1404
- {
1405
- "key": {
1406
- "el": "Διορισμός εκπαιδευτικού",
1407
- "en": "Teachers appointment",
1408
- "tr": ""
1409
- },
1410
- "value": [
1411
- {
1412
- "element": "summaryList",
1413
- "params": {
1414
- "items": [
1415
- {
1416
- "key": {
1417
- "el": "Τι διορισμό έχει ο εκπαιδευτικός;",
1418
- "en": "What type of appointment does the teacher have?"
1419
- },
1420
- "value": [
1421
- {
1422
- "element": "textElement",
1423
- "params": {
1424
- "text": {
1425
- "en": "Συμβασιούχος",
1426
- "el": "Συμβασιούχος",
1427
- "tr": "Συμβασιούχος"
1428
- },
1429
- "type": "span"
1430
- }
1431
- }
1432
- ]
1433
- },
1434
- {
1435
- "key": {
1436
- "el": "Αριθμός φακέλου (ΠΜΠ)",
1437
- "en": "File Number"
1438
- },
1439
- "value": [
1440
- {
1441
- "element": "textElement",
1442
- "params": {
1443
- "text": {
1444
- "en": "1212",
1445
- "el": "1212",
1446
- "tr": "1212"
1447
- },
1448
- "type": "span"
1449
- }
1450
- }
1451
- ]
1452
- },
1453
- {
1454
- "key": {
1455
- "el": "Ειδικότητα",
1456
- "en": "Specialty"
1457
- },
1458
- "value": [
1459
- {
1460
- "element": "textElement",
1461
- "params": {
1462
- "text": {
1463
- "en": "Καθηγητής",
1464
- "el": "Καθηγητής",
1465
- "tr": "Καθηγητής"
1466
- },
1467
- "type": "span"
1468
- }
1469
- }
1470
- ]
1471
- }
1472
- ]
1473
- }
1474
- }
1475
- ]
1476
- },
1477
- {
1478
- "key": {
1479
- "el": "Ημερομηνία ανάληψης",
1480
- "en": "Takeover date",
1481
- "tr": ""
1482
- },
1483
- "value": [
1484
- {
1485
- "element": "summaryList",
1486
- "params": {
1487
- "items": [
1488
- {
1489
- "key": {
1490
- "el": "Ημερομηνία ανάληψης",
1491
- "en": "Start Date"
1492
- },
1493
- "value": [
1494
- {
1495
- "element": "textElement",
1496
- "params": {
1497
- "text": {
1498
- "en": "16/04/2025",
1499
- "el": "16/04/2025",
1500
- "tr": "16/04/2025"
1501
- },
1502
- "type": "span"
1503
- }
1504
- }
1505
- ]
1506
- },
1507
- {
1508
- "key": {
1509
- "el": "Η ημερομηνία αυτή είναι η ίδια με αυτή του συμβολαίου;",
1510
- "en": "Is this date the same as the contract date?"
1511
- },
1512
- "value": [
1513
- {
1514
- "element": "textElement",
1515
- "params": {
1516
- "text": {
1517
- "en": "Ναι, είναι η ίδια με αυτή του συμβολαίου",
1518
- "el": "Ναι, είναι η ίδια με αυτή του συμβολαίου",
1519
- "tr": "Ναι, είναι η ίδια με αυτή του συμβολαίου"
1520
- },
1521
- "type": "span"
1522
- }
1523
- }
1524
- ]
1525
- }
1526
- ]
1527
- }
1528
- }
1529
- ]
1530
- }
1531
- ]
1532
- }
1571
+ ...
1533
1572
  },
1534
1573
  "printFriendlyData": [ // Print friendly data. Object, will be stringified
1535
- {
1536
- "pageUrl": "index", // Page URL
1537
- "pageTitle": { // Page title
1538
- "el": "Στοιχεία του εκπαιδευτικού",
1539
- "en": "Educator's details",
1540
- "tr": ""
1541
- },
1542
- "fields": [ // Fields
1543
- {
1544
- "id": "id_select", // Field ID
1545
- "label": { // Field label
1546
- "el": "Ταυτοποίηση",
1547
- "en": "Identification"
1548
- },
1549
- "value": ["id", "arc"], // Field value. // field level: checkboxes are ALWAYS arrays (may be []); radios/select/text are strings
1550
- "valueLabel": [ // Field value label
1551
- {
1552
- "el": "Ταυτότητα",
1553
- "en": "ID",
1554
- "tr": ""
1555
- },
1556
- {
1557
- "el": "ARC",
1558
- "en": "ARC",
1559
- "tr": ""
1560
- }
1561
- ]
1562
- },
1563
- {
1564
- "id": "id_number",
1565
- "label": {
1566
- "el": "Εισαγάγετε αριθμό ταυτότητας",
1567
- "en": "Enter ID number"
1568
- },
1569
- "value": "654654",
1570
- "valueLabel": {
1571
- "el": "654654",
1572
- "en": "654654"
1573
- }
1574
- },
1575
- {
1576
- "id": "aka",
1577
- "label": {
1578
- "el": "Αριθμός κοινωνικών ασφαλίσεων",
1579
- "en": "Social Insurance Number"
1580
- },
1581
- "value": "232323",
1582
- "valueLabel": {
1583
- "el": "232323",
1584
- "en": "232323"
1585
- }
1586
- }
1587
- ]
1588
- },
1589
- {
1590
- "pageUrl": "appointment",
1591
- "pageTitle": {
1592
- "el": "Διορισμός εκπαιδευτικού",
1593
- "en": "Teachers appointment",
1594
- "tr": ""
1595
- },
1596
- "fields": [
1597
- {
1598
- "id": "diorismos",
1599
- "label": {
1600
- "el": "Τι διορισμό έχει ο εκπαιδευτικός;",
1601
- "en": "What type of appointment does the teacher have?"
1602
- },
1603
- "value": "monimos",
1604
- "valueLabel": {
1605
- "el": "Μόνιμος επί δοκιμασία",
1606
- "en": "Permanent on probation",
1607
- "tr": ""
1608
- }
1609
- },
1610
- {
1611
- "id": "fileno_monimos",
1612
- "label": {
1613
- "el": "Αριθμός φακέλου (ΠΜΠ)",
1614
- "en": "File Number"
1615
- },
1616
- "value": "3233",
1617
- "valueLabel": {
1618
- "el": "3233",
1619
- "en": "3233"
1620
- }
1621
- },
1622
- {
1623
- "id": "eidikotita_monimos",
1624
- "label": {
1625
- "el": "Ειδικότητα",
1626
- "en": "Specialty"
1627
- },
1628
- "value": "1",
1629
- "valueLabel": {
1630
- "el": "Δάσκαλος",
1631
- "en": "Elementary teacher",
1632
- "tr": ""
1633
- }
1634
- }
1635
- ]
1636
- },
1637
- {
1638
- "pageUrl": "takeover",
1639
- "pageTitle": {
1640
- "el": "Ημερομηνία ανάληψης",
1641
- "en": "Takeover date",
1642
- "tr": ""
1643
- },
1644
- "fields": [
1645
- {
1646
- "id": "date_start",
1647
- "label": {
1648
- "el": "Ημερομηνία ανάληψης",
1649
- "en": "Start Date"
1650
- },
1651
- "value": "2020-12-11",
1652
- "valueLabel": {
1653
- "el": "11/12/2020",
1654
- "en": "11/12/2020"
1655
- }
1656
- },
1657
- {
1658
- "id": "date_on_contract",
1659
- "label": {
1660
- "el": "Η ημερομηνία αυτή είναι η ίδια με αυτή του συμβολαίου;",
1661
- "en": "Is this date the same as the contract date?"
1662
- },
1663
- "value": "date_other",
1664
- "valueLabel": {
1665
- "el": "Όχι, αυτή είναι διαφορετική",
1666
- "en": "No, this is different",
1667
- "tr": ""
1668
- }
1669
- },
1670
- {
1671
- "id": "date_contract",
1672
- "label": {
1673
- "el": "Ημερομηνία συμβολαίου",
1674
- "en": "Contract Date"
1675
- },
1676
- "value": "16/04/2025",
1677
- "valueLabel": {
1678
- "el": "16/04/2025",
1679
- "en": "16/04/2025"
1680
- }
1681
- },
1682
- {
1683
- "id": "reason",
1684
- "label": {
1685
- "el": "Αιτιολόγηση καθυστέρησης στην ανάληψη καθηκόντων",
1686
- "en": "Reason for delay in assuming duties"
1687
- },
1688
- "value": "24324dssf",
1689
- "valueLabel": {
1690
- "el": "24324dssf",
1691
- "en": "24324dssf"
1692
- }
1693
- }
1694
- ]
1695
- }
1574
+ ...
1696
1575
  ],
1697
1576
  "rendererVersion": "1.14.1", // Renderer version
1698
1577
  "designSystemsVersion": "3.1.0", // Design systems version
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gov-cy/govcy-express-services",
3
- "version": "1.3.0-alpha",
3
+ "version": "1.3.0-alpha.2",
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",
@@ -51,8 +51,8 @@
51
51
  "coverage:badge": "coverage-badges --output ./coverage-badges.svg && npm run coverage:copy"
52
52
  },
53
53
  "dependencies": {
54
- "@gov-cy/dsf-email-templates": "^2.1.0",
55
- "@gov-cy/govcy-frontend-renderer": "^1.25.0",
54
+ "@gov-cy/dsf-email-templates": "^2.1.1",
55
+ "@gov-cy/govcy-frontend-renderer": "^1.26.0",
56
56
  "axios": "^1.9.0",
57
57
  "cookie-parser": "^1.4.7",
58
58
  "dotenv": "^16.3.1",
@@ -77,5 +77,8 @@
77
77
  },
78
78
  "engines": {
79
79
  "node": ">=18.0.0"
80
+ },
81
+ "overrides": {
82
+ "tar-fs": "^3.1.1"
80
83
  }
81
84
  }
@@ -132,6 +132,10 @@ export function govcyFileDeletePageHandler() {
132
132
  } else {
133
133
  // normal page
134
134
  actionPath = `${pageUrl}/delete-file/${elementName}`;
135
+ // if normal page but has multipleThings, block it
136
+ if (page?.multipleThings) {
137
+ return handleMiddlewareError(`Single mode delete file not allowed on multipleThings pages`, 404, next)
138
+ }
135
139
  }
136
140
  // Construct submit button
137
141
  const formElement = {
@@ -243,6 +247,10 @@ export function govcyFileDeletePostHandler() {
243
247
  } else {
244
248
  // normal page
245
249
  actionPath = `${pageUrl}`;
250
+ // if normal page but has multipleThings, block it
251
+ if (page?.multipleThings) {
252
+ return handleMiddlewareError(`Single mode delete file not allowed on multipleThings pages`, 404, next)
253
+ }
246
254
  }
247
255
 
248
256
  // the page base return url
@@ -62,6 +62,11 @@ export function govcyFileViewHandler() {
62
62
  // return handleMiddlewareError(`Page condition evaluated to true on POST — skipping form save and redirecting`, 404, next);
63
63
  // }
64
64
 
65
+
66
+ // If mode is `single` make sure it has no multipleThings
67
+ if (mode === "single" && page?.multipleThings) {
68
+ return handleMiddlewareError(`Single mode file view not allowed on multipleThings pages`, 404, next)
69
+ }
65
70
  // Validate the field: Only allow delete if the page contains a fileInput with the given name
66
71
  const fileInputElement = pageContainsFileInput(pageTemplateCopy, elementName);
67
72
  if (!fileInputElement) {
@@ -63,6 +63,8 @@ export function govcyHttpErrorHandler(err, req, res, next) {
63
63
  if (dataLayer.getUser(req.session)) {
64
64
  pageTemplate.sections.push(govcyResources.userNameSection(dataLayer.getUser(req.session).name)); // Add user name section
65
65
  }
66
+ // Add custom CSS path
67
+ pageData.site.customCSSFile = `/css/govcyExpress.css`;
66
68
  const html = renderer.renderFromJSON(pageTemplate, pageData);
67
69
  res.send(html);
68
70
  }
@@ -81,8 +81,8 @@ export function govcyMultipleThingsDeletePageHandler() {
81
81
  isPageHeading: true,
82
82
  classes: "govcy-mb-6",
83
83
  items: [
84
- { value: "yes", text: govcyResources.staticResources.text.deleteYesOption },
85
- { value: "no", text: govcyResources.staticResources.text.deleteNoOption }
84
+ { value: "yes", text: govcyResources.staticResources.text.multipleThingsDeleteYesOption },
85
+ { value: "no", text: govcyResources.staticResources.text.multipleThingsDeleteNoOption }
86
86
  ]
87
87
  }
88
88
  };
@@ -24,6 +24,8 @@ export function renderGovcyPage() {
24
24
  if (dataLayer.getUser(req.session)) {
25
25
  processedPage.pageTemplate.sections.push(govcyResources.userNameSection(dataLayer.getUser(req.session).name)); // Add user name section
26
26
  }
27
+ // Add custom CSS path
28
+ processedPage.pageData.site.customCSSFile = `/css/govcyExpress.css`;
27
29
  const html = renderer.renderFromJSON(processedPage.pageTemplate, processedPage.pageData);
28
30
  res.send(html);
29
31
  };
@@ -47,6 +47,8 @@ export function govcyRoutePageHandler(req, res, next) {
47
47
  if (dataLayer.getUser(req.session)) {
48
48
  pageTemplate.sections.push(govcyResources.userNameSection(dataLayer.getUser(req.session).name)); // Add user name section
49
49
  }
50
+ // Add custom CSS path
51
+ pageData.site.customCSSFile = `/css/govcyExpress.css`;
50
52
  const renderer = new govcyFrontendRenderer();
51
53
  const html = renderer.renderFromJSON(pageTemplate, pageData);
52
54
  res.send(html);
@@ -0,0 +1,33 @@
1
+
2
+ dl.govcy-summary-list-row-internal:not(:first-of-type) {
3
+ margin-top: 0.5rem !important;
4
+ }
5
+
6
+ .list-inline-item:not(:last-child) {
7
+ margin-right: 0 !important;
8
+ }
9
+ .list-inline-item:not(:first-child) {
10
+ margin-left: .5rem;
11
+ }
12
+
13
+ .govcy-add-new-item {
14
+ display: inline-flex;
15
+ align-items: center;
16
+ text-decoration: underline;
17
+ }
18
+
19
+ .govcy-add-new-item::before {
20
+ content: "";
21
+ display: inline-block;
22
+ width: 24px;
23
+ height: 24px;
24
+ background: url("/img/Plus_24x24.svg") no-repeat center center;
25
+ background-size: contain;
26
+ margin-right: 0.25rem; /* Fallback for browsers without flex gap */
27
+ }
28
+
29
+ @media (max-width: 767.98px) {
30
+ #multipleThingsList>tbody>tr>td {
31
+ padding: .5rem .5rem;
32
+ }
33
+ }
@@ -172,9 +172,9 @@ export const staticResources = {
172
172
  tr: "You did not add any entries yet."
173
173
  },
174
174
  multipleThingsAddEntry: {
175
- en: "Add new entry",
176
- el: "Προσθήκη νέας καταχώρησης",
177
- tr: "Add new entry"
175
+ en: "Add new entry",
176
+ el: "Προσθήκη νέας καταχώρησης",
177
+ tr: "Add new entry"
178
178
  },
179
179
  multipleThingsDedupeMessage: {
180
180
  en: "This entry already exists",
@@ -220,6 +220,16 @@ export const staticResources = {
220
220
  en: "Entries",
221
221
  el: "Καταχωρήσεις",
222
222
  tr: "Entries"
223
+ },
224
+ multipleThingsDeleteYesOption: {
225
+ el:"Ναι, θέλω να διαγράψω την καταχώρηση",
226
+ en:"Yes, I want to delete this entry",
227
+ tr:"Yes, I want to delete this entry"
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"
223
233
  }
224
234
  },
225
235
  //remderer sections
@@ -631,9 +641,9 @@ export function getMultipleThingsLink(linkType, siteId, pageUrl, lang , entryKey
631
641
  element: "htmlElement",
632
642
  params: {
633
643
  text: {
634
- en: `<p><a${(count !== null && linkType === "add" ? ` id="addNewItem${count}"` : "")} href="${fullPath}">${linkTextString}</a></p>`,
635
- el: `<p><a${(count !== null && linkType === "add" ? ` id="addNewItem${count}"` : "")} href="${fullPath}">${linkTextString}</a></p>`,
636
- tr: `<p><a${(count !== null && linkType === "add" ? ` id="addNewItem${count}"` : "")} href="${fullPath}">${linkTextString}</a></p>`
644
+ en: `<p><a${(count !== null && linkType === "add" ? ` class="govcy-add-new-item" id="addNewItem${count}"` : "")} href="${fullPath}">${linkTextString}</a></p>`,
645
+ el: `<p><a${(count !== null && linkType === "add" ? ` class="govcy-add-new-item" id="addNewItem${count}"` : "")} href="${fullPath}">${linkTextString}</a></p>`,
646
+ tr: `<p><a${(count !== null && linkType === "add" ? ` class="govcy-add-new-item" id="addNewItem${count}"` : "")} href="${fullPath}">${linkTextString}</a></p>`
637
647
  }
638
648
  }
639
649
  };
@@ -104,7 +104,13 @@ export function populateFormData(
104
104
 
105
105
  // 2) If not found, fall back to dataLayer (normal page behaviour)
106
106
  if (!fileData) {
107
- fileData = dataLayer.getFormDataValue(store, siteId, pageUrl, fieldName);
107
+ if (mode === "edit" && index !== null) {
108
+ // In edit mode, try to get the file for the specific item index
109
+ fileData = dataLayer.getFormDataValue(store, siteId, pageUrl, fieldName, index);
110
+ } else {
111
+ // In single or add mode, get the file normally
112
+ fileData = dataLayer.getFormDataValue(store, siteId, pageUrl, fieldName);
113
+ }
108
114
  }
109
115
 
110
116
  if (fileData) {
@@ -100,6 +100,16 @@ export async function handleFileUpload({
100
100
 
101
101
  // deep copy the page template to avoid modifying the original
102
102
  const pageTemplateCopy = JSON.parse(JSON.stringify(page.pageTemplate));
103
+
104
+ // If mode is `single` make sure it has no multipleThings
105
+ if (mode === "single" && page?.multipleThings) {
106
+ return {
107
+ status: 400,
108
+ dataStatus: 413,
109
+ errorMessage: 'Single mode upload not allowed on multipleThings pages'
110
+ };
111
+ }
112
+
103
113
  // Validate the field: Only allow upload if the page contains a fileInput with the given name
104
114
  const isAllowed = pageContainsFileInput(pageTemplateCopy, elementName);
105
115
  if (!isAllowed) {
@@ -96,13 +96,39 @@ function validateValue(value, rules) {
96
96
  }
97
97
  return normalizedVal <= max;
98
98
  },
99
- // ✅ New rule: maxCurrentYear
99
+ // ✅ Year based current rules
100
100
  maxCurrentYear: (val) => {
101
101
  const normalizedVal = normalizeNumber(val);
102
102
  if (isNaN(normalizedVal)) return false;
103
103
  const currentYear = new Date().getFullYear();
104
104
  return normalizedVal <= currentYear;
105
105
  },
106
+ minCurrentYear: (val) => {
107
+ const normalizedVal = normalizeNumber(val);
108
+ if (isNaN(normalizedVal)) return false;
109
+ const currentYear = new Date().getFullYear();
110
+ return normalizedVal >= currentYear;
111
+ },
112
+ // ✅ Date-based current rules
113
+ minCurrentDate: (val) => {
114
+ const valueDate = parseDate(val);
115
+ const today = new Date();
116
+ if (isNaN(valueDate)) return false;
117
+ // strip time components from both
118
+ const valueOnly = new Date(valueDate.getFullYear(), valueDate.getMonth(), valueDate.getDate());
119
+ const todayOnly = new Date(today.getFullYear(), today.getMonth(), today.getDate());
120
+ return valueOnly >= todayOnly;
121
+ },
122
+
123
+ maxCurrentDate: (val) => {
124
+ const valueDate = parseDate(val);
125
+ const today = new Date();
126
+ if (isNaN(valueDate)) return false;
127
+ const valueOnly = new Date(valueDate.getFullYear(), valueDate.getMonth(), valueDate.getDate());
128
+ const todayOnly = new Date(today.getFullYear(), today.getMonth(), today.getDate());
129
+ return valueOnly <= todayOnly;
130
+ },
131
+
106
132
  minValueDate: (val, minDate) => {
107
133
  const valueDate = parseDate(val); // Parse the input date
108
134
  const min = parseDate(minDate); // Parse the minimum date
@@ -139,7 +165,7 @@ function validateValue(value, rules) {
139
165
  // Skip validation if the value is empty
140
166
  if (value === null || value === undefined || (typeof value === 'string' && value.trim() === "")) {
141
167
  continue; // let "required" handle emptiness
142
- }
168
+ }
143
169
  // Check for "valid" rules (e.g., numeric, telCY, etc.)
144
170
  if (check === "valid" && validationRules[checkValue]) {
145
171
  const isValid = validationRules[checkValue](value);
@@ -168,12 +194,12 @@ function validateValue(value, rules) {
168
194
  if (check === 'minValue' && !validationRules.minValue(value, checkValue)) {
169
195
  return message;
170
196
  }
171
-
197
+
172
198
  // Check for "maxValue"
173
199
  if (check === 'maxValue' && !validationRules.maxValue(value, checkValue)) {
174
200
  return message;
175
201
  }
176
-
202
+
177
203
  // Check for "minValueDate"
178
204
  if (check === 'minValueDate' && !validationRules.minValueDate(value, checkValue)) {
179
205
  return message;