diff --git a/frontend/components.d.ts b/frontend/components.d.ts index 4e48c7aae0..750c656ffa 100644 --- a/frontend/components.d.ts +++ b/frontend/components.d.ts @@ -3,16 +3,16 @@ // @ts-nocheck // Generated by unplugin-vue-components // Read more: https://github.com/vuejs/core/pull/3399 -import '@vue/runtime-core' - export {} -declare module '@vue/runtime-core' { +declare module 'vue' { export interface GlobalComponents { AddressGroupComponent: typeof import('./src/components/grouping/AddressGroupComponent.vue')['default'] AutoCompleteInputComponent: typeof import('./src/components/forms/AutoCompleteInputComponent.vue')['default'] ContactGroupComponent: typeof import('./src/components/grouping/ContactGroupComponent.vue')['default'] DataFetcher: typeof import('./src/components/DataFetcher.vue')['default'] + DateInputComponent: typeof import('./src/components/forms/DateInputComponent/index.vue')['default'] + DateInputPart: typeof import('./src/components/forms/DateInputComponent/DateInputPart.vue')['default'] DropdownInputComponent: typeof import('./src/components/forms/DropdownInputComponent.vue')['default'] ErrorNotificationGroupingComponent: typeof import('./src/components/grouping/ErrorNotificationGroupingComponent.vue')['default'] MainHeaderComponent: typeof import('./src/components/MainHeaderComponent.vue')['default'] diff --git a/frontend/cypress/e2e/FormGeneral.cy.ts b/frontend/cypress/e2e/FormGeneral.cy.ts index 3f337e7550..c18ac55171 100644 --- a/frontend/cypress/e2e/FormGeneral.cy.ts +++ b/frontend/cypress/e2e/FormGeneral.cy.ts @@ -50,6 +50,12 @@ describe("General Form", () => { cy.wait("@selectCompany"); + cy.get("#birthdate").should("be.visible"); + + cy.get("#birthdateYear").shadow().find("input").should("have.value", "").type("2001"); + cy.get("#birthdateMonth").shadow().find("input").should("have.value", "").type("05"); + cy.get("#birthdateDay").shadow().find("input").should("have.value", "").type("30"); + cy.get('[data-test="wizard-next-button"]').should("be.visible").click(); cy.logout(); diff --git a/frontend/src/assets/styles/global.scss b/frontend/src/assets/styles/global.scss index eb1318e295..13ee90dd5a 100644 --- a/frontend/src/assets/styles/global.scss +++ b/frontend/src/assets/styles/global.scss @@ -1208,6 +1208,10 @@ cds-header-panel[expanded] { transition: opacity 300ms cubic-bezier(0.5, 0, 0.1, 1), background-color 300ms cubic-bezier(0.5, 0, 0.1, 1); } +.date-label { + margin-bottom: 1rem; +} + /* Small (up to 671px) */ diff --git a/frontend/src/components/forms/DateInputComponent/DateInputPart.vue b/frontend/src/components/forms/DateInputComponent/DateInputPart.vue new file mode 100644 index 0000000000..284b1e9b27 --- /dev/null +++ b/frontend/src/components/forms/DateInputComponent/DateInputPart.vue @@ -0,0 +1,66 @@ + + + diff --git a/frontend/src/components/forms/DateInputComponent/common.ts b/frontend/src/components/forms/DateInputComponent/common.ts new file mode 100644 index 0000000000..ba502d631f --- /dev/null +++ b/frontend/src/components/forms/DateInputComponent/common.ts @@ -0,0 +1,5 @@ +export enum DatePart { + year, + month, + day, +}; diff --git a/frontend/src/components/forms/DateInputComponent/index.vue b/frontend/src/components/forms/DateInputComponent/index.vue new file mode 100644 index 0000000000..ecad702736 --- /dev/null +++ b/frontend/src/components/forms/DateInputComponent/index.vue @@ -0,0 +1,474 @@ + + + + + diff --git a/frontend/src/helpers/validators/BCeIDFormValidations.ts b/frontend/src/helpers/validators/BCeIDFormValidations.ts index d7f292c4b4..d671e88d44 100644 --- a/frontend/src/helpers/validators/BCeIDFormValidations.ts +++ b/frontend/src/helpers/validators/BCeIDFormValidations.ts @@ -10,6 +10,9 @@ import { isNoSpecialCharacters, formFieldValidations, isNotEmptyArray, + isMinimumYearsAgo, + isDateInThePast, + isGreaterThan, } from "@/helpers/validators/GlobalValidators"; @@ -18,6 +21,15 @@ formFieldValidations["businessInformation.businessName"] = [ isNotEmpty("Business Name cannot be empty"), ]; +formFieldValidations["businessInformation.birthdate"] = [ + isDateInThePast("Date of birth must be in the past"), + isMinimumYearsAgo(19, "You must be at least 19 years old to apply"), +]; + +formFieldValidations["businessInformation.birthdate.year"] = [ + isGreaterThan(1899, "Please check the birth year"), +]; + // Step 2: Addresses formFieldValidations["location.addresses.*.locationName"] = [ isNotEmpty("You must provide a name for this location"), diff --git a/frontend/src/helpers/validators/GlobalValidators.ts b/frontend/src/helpers/validators/GlobalValidators.ts index 3e8bcb697a..d65cf414f4 100644 --- a/frontend/src/helpers/validators/GlobalValidators.ts +++ b/frontend/src/helpers/validators/GlobalValidators.ts @@ -1,5 +1,9 @@ import type { Ref } from "vue"; import { useEventBus } from "@vueuse/core"; +import subYears from "date-fns/subYears"; +import startOfToday from "date-fns/startOfToday"; +import isBefore from "date-fns/isBefore"; +import parseISO from "date-fns/parseISO"; import type { ValidationMessageType } from "@/dto/CommonTypesDto"; // Defines the used regular expressions @@ -274,6 +278,100 @@ export const isNot = return message; }; +export const isWithinRange = + (minValue: number, maxValue: number, message = "Value is out of range") => + (value: number | string): string => { + if (value >= minValue && value <= maxValue) return ""; + return message; + }; + +/** + * Checks if the value is a possibly valid day for the specified month. + * Note: February 29 will always be considered valid, since this validation does not consider the year. + * + * @param validMonth a valid month + * @param message the error message to be returned if the validation fails. + */ +export const isValidDayOfMonth = + ( + validMonth: string, + message = "Value is not a valid day in the selected month", + ) => + (value: string): string => { + const arbitraryLeapYear = 2000; + const dateString = `${arbitraryLeapYear}-${validMonth}-${value}`; + const date = parseISO(dateString); + if (isNaN(date.getTime())) return message; + const isoStringDate = date.toISOString().substring(0, 10); + if (isoStringDate !== dateString) return message; + return ""; + }; + +/** + * Checks if the value is a valid day for the specified year and month. + * i.e. it tells if the date formed by the provided year, month and day exists. + * + * @param validYear a valid year + * @param validMonth a valid month + * @param message the error message to be returned if the validation fails. + */ +export const isValidDayOfMonthYear = + ( + validYear: string, + validMonth: string, + message = "Value is not a valid day in the selected month and year", + ) => + (value: string): string => { + const dateString = `${validYear}-${validMonth}-${value}`; + const date = parseISO(dateString); + if (isNaN(date.getTime())) return message; + const isoStringDate = date.toISOString().substring(0, 10); + if (isoStringDate !== dateString) return message; + return ""; + }; + +export const isMinimumYearsAgo = + ( + years: number, + message: string | ((years: number) => string) = (years) => + `Value must be at least ${years} years ago`, + ) => + (value: string): string => { + const maximumDate = subYears(startOfToday(), years); + const valueDate = parseISO(value); + if (valueDate > maximumDate) { + if (typeof message === "function") { + return message(years); + } + return message; + } + return ""; + }; + +export const isGreaterThan = + ( + compareTo: number, + message: string | ((year: number) => string) = (compareTo) => + `Value must be greater than ${compareTo}`, + ) => + (value: string): string => { + if (Number(value) > compareTo) { + return ""; + } + if (typeof message === "function") { + return message(compareTo); + } + return message; + }; + +export const isDateInThePast = (message: "Value must be in the past") => (value: string) => { + const dateValue = parseISO(value); + if (!isBefore(dateValue, startOfToday())) { + return message; + } + return ""; +}; + // This function will extract the field value from a DTO object export const getFieldValue = (path: string, value: any): string | string[] => { // First we set is in a temporary variable diff --git a/frontend/src/pages/bceidform/BusinessInformationWizardStep.vue b/frontend/src/pages/bceidform/BusinessInformationWizardStep.vue index b47bed96a4..0a022de3c1 100644 --- a/frontend/src/pages/bceidform/BusinessInformationWizardStep.vue +++ b/frontend/src/pages/bceidform/BusinessInformationWizardStep.vue @@ -53,6 +53,7 @@ watch( const validation = reactive>({ businessType: !!formData.value.businessInformation.businessType, business: !!formData.value.businessInformation.businessName, + birthdate: !!formData.value.businessInformation.birthdate, }); const checkValid = () => @@ -200,6 +201,25 @@ watch([selectedOption], () => { showAutoCompleteInfo.value = true; } }); + +const showBirthDate = computed( + () => + validation.business && + (selectedOption.value === BusinessTypeEnum.U || + formData.value.businessInformation.clientType === ClientTypeEnum[ClientTypeEnum.RSP]), +); + +watch(showBirthDate, (value) => { + if (value) { + validation.birthdate = !!formData.value.businessInformation.birthdate; + } else { + // Reset birth date. + formData.value.businessInformation.birthdate = ""; + + // Consider birth date valid so it doesn't interfere with the overall validation status. + validation.birthdate = true; + } +});