diff --git a/src/ECER.Clients.RegistryPortal/ECER.Clients.RegistryPortal.Server/ConfigurationEndpoints.cs b/src/ECER.Clients.RegistryPortal/ECER.Clients.RegistryPortal.Server/ConfigurationEndpoints.cs index c0cd7ffb8..a2efdeaa7 100644 --- a/src/ECER.Clients.RegistryPortal/ECER.Clients.RegistryPortal.Server/ConfigurationEndpoints.cs +++ b/src/ECER.Clients.RegistryPortal/ECER.Clients.RegistryPortal.Server/ConfigurationEndpoints.cs @@ -1,4 +1,5 @@ using ECER.Clients.RegistryPortal.Server.Shared; +using ECER.Infrastructure.Common.Validators; using ECER.Managers.Admin.Contract.Metadatas; using ECER.Utilities.Hosting; using Mediator; @@ -54,6 +55,7 @@ public void Register(IEndpointRouteBuilder endpointRouteBuilder) var results = await messageBus.Send(new CertificationComparisonQuery() { ById = id, ByProvinceId = provinceId }, ct); return TypedResults.Ok(configurationMapper.MapComparisonRecords(results.Items)); }) + .AddGuidValidationQueryParams(["provinceId"], false) .WithOpenApi("Handles certification comparison queries", string.Empty, "certificationComparison_get"); endpointRouteBuilder.MapGet("/api/postSecondaryInstitutionList/{id?}", async (string? id, string? name, string? provinceId, PostSecondaryInstitutionStatus? status, IMediator messageBus, IConfigurationMapper configurationMapper, CancellationToken ct) => @@ -62,6 +64,7 @@ public void Register(IEndpointRouteBuilder endpointRouteBuilder) var results = await messageBus.Send(new PostSecondaryInstitutionsQuery() { ById = id, ByName = name, ByProvinceId = provinceId, ByStatus = status }, ct); return TypedResults.Ok(configurationMapper.MapPostSecondaryInstitutions(results.Items)); }) + .AddGuidValidationQueryParams(["provinceId"], false) .WithOpenApi("Handles psi queries", string.Empty, "psi_get"); endpointRouteBuilder.MapGet("/api/systemMessages", async (HttpContext ctx, IMediator messageBus, IConfigurationMapper configurationMapper, CancellationToken ct) => @@ -74,11 +77,12 @@ public void Register(IEndpointRouteBuilder endpointRouteBuilder) endpointRouteBuilder.MapGet("/api/identificationTypes", async ([AsParameters] IdentificationTypesQuery request, HttpContext ctx, IMediator messageBus, IConfigurationMapper configurationMapper, CancellationToken ct) => { - var query = new Managers.Admin.Contract.Metadatas.IdentificationTypesQuery() { ById = request.ById, ForPrimary = request.ForPrimary, ForSecondary = request.ForSecondary }; + var query = new Managers.Admin.Contract.Metadatas.IdentificationTypesQuery() { ById = string.IsNullOrWhiteSpace(request.ById) ? null : request.ById, ForPrimary = request.ForPrimary, ForSecondary = request.ForSecondary }; var results = await messageBus.Send(query, ct); return TypedResults.Ok(configurationMapper.MapIdentificationTypes(results.Items)); }) .WithOpenApi("Handles identification types queries", string.Empty, "identificationTypes_get") + .AddGuidValidationQueryParams(["ById"], false) .CacheOutput(p => p.Expire(TimeSpan.FromMinutes(5))); endpointRouteBuilder.MapGet("/api/captchaSiteKey", async (IOptions captchaSettings, CancellationToken ct) => diff --git a/src/ECER.Clients.RegistryPortal/ecer.clients.registryportal.client/.storybook/preview.ts b/src/ECER.Clients.RegistryPortal/ecer.clients.registryportal.client/.storybook/preview.ts index 402ef94b9..18635dd6b 100644 --- a/src/ECER.Clients.RegistryPortal/ecer.clients.registryportal.client/.storybook/preview.ts +++ b/src/ECER.Clients.RegistryPortal/ecer.clients.registryportal.client/.storybook/preview.ts @@ -5,6 +5,7 @@ import { setup } from "@storybook/vue3-vite"; // ✅ Use the setup function from import { createVuetify } from "vuetify"; import "vuetify/styles"; // Import Vuetify base styles import "@mdi/font/css/materialdesignicons.css"; // Optional: MDI icons +import { sb } from "storybook/test"; import * as components from "vuetify/components"; import * as directives from "vuetify/directives"; @@ -16,6 +17,8 @@ import { createPinia } from "pinia"; import "../src/styles/main.scss"; +sb.mock(import("../src/api/configuration.ts"), { spy: true }); // Used to mock api calls + // ✅ Global setup: Register Vuetify and Pinia setup((app) => { const vuetify = createVuetify({ diff --git a/src/ECER.Clients.RegistryPortal/ecer.clients.registryportal.client/src/components/ApplicationRequirements.vue b/src/ECER.Clients.RegistryPortal/ecer.clients.registryportal.client/src/components/ApplicationRequirements.vue index 508c3c4d8..0f63ce651 100644 --- a/src/ECER.Clients.RegistryPortal/ecer.clients.registryportal.client/src/components/ApplicationRequirements.vue +++ b/src/ECER.Clients.RegistryPortal/ecer.clients.registryportal.client/src/components/ApplicationRequirements.vue @@ -37,9 +37,21 @@ /> + + + + @@ -104,6 +116,9 @@ import ECEOneYearRenewalRequirements from "@/components/ECEOneYearRenewalRequire import ECEAssistantLaborMobilityRequirements from "./ECEAssistantLaborMobilityRequirements.vue"; import ECEOneYearLaborMobilityRequirements from "./ECEOneYearLaborMobilityRequirements.vue"; import ECEFiveYearLaborMobilityRequirements from "./ECEFiveYearLaborMobilityRequirements.vue"; +import ECESneLabourMobilityRequirements from "./ECESneLabourMobilityRequirements.vue"; +import ECEIteLabourMobilityRequirements from "./ECEIteLabourMobilityRequirements.vue"; +import ECESneAndIteLabourMobilityRequirements from "./ECESneAndIteLabourMobilityRequirements.vue"; import ECEOneYearRequirements from "@/components/ECEOneYearRequirements.vue"; import { useApplicationStore } from "@/store/application"; import { useCertificationStore } from "@/store/certification"; @@ -125,6 +140,9 @@ export default defineComponent({ ECEAssistantLaborMobilityRequirements, ECEOneYearLaborMobilityRequirements, ECEFiveYearLaborMobilityRequirements, + ECESneLabourMobilityRequirements, + ECEIteLabourMobilityRequirements, + ECESneAndIteLabourMobilityRequirements, ECEAssistantRenewalRequirements, ECEOneYearRenewalRequirements, ECEFiveYearRenewalRequirements, @@ -200,21 +218,6 @@ export default defineComponent({ this.router.push({ name: "declaration" }); } } else if (this.applicationStore.isDraftApplicationLaborMobility) { - // handle case where user chooses 5 year specializations - if ( - this.applicationStore.isDraftCertificateTypeFiveYears && - this.applicationStore.isDraftCertificateTypeIte && - this.applicationStore.isDraftCertificateTypeSne - ) { - this.applicationStore.$patch({ - draftApplication: { - certificationTypes: [ - "FiveYears", - ...this.specializationSelection, - ], - }, - }); - } this.router.push({ name: "declaration" }); } else if (this.applicationStore.isDraftApplicationRenewal) { //for renewal applications we do not need to perform any additional checks. The certification types should be correctly set in the draft application store. diff --git a/src/ECER.Clients.RegistryPortal/ecer.clients.registryportal.client/src/components/Breadcrumb.vue b/src/ECER.Clients.RegistryPortal/ecer.clients.registryportal.client/src/components/Breadcrumb.vue index 3f2e30481..7d48b039d 100644 --- a/src/ECER.Clients.RegistryPortal/ecer.clients.registryportal.client/src/components/Breadcrumb.vue +++ b/src/ECER.Clients.RegistryPortal/ecer.clients.registryportal.client/src/components/Breadcrumb.vue @@ -554,6 +554,16 @@ export default defineComponent({ }, ]; + case "application-transfer": + return [ + ...this.baseItems, + { + title: "Check your transfer eligibility", + disabled: true, + href: "/application/transfer", + }, + ]; + case "icra-eligibility": return [ ...this.baseItems, @@ -690,7 +700,6 @@ export default defineComponent({ isApplicationRoute(routeName: string): boolean { return [ "application-certification", - "application-transfer", "application-requirements", "consent-required", "declaration", diff --git a/src/ECER.Clients.RegistryPortal/ecer.clients.registryportal.client/src/components/ECEFiveYearLaborMobilityRequirements.vue b/src/ECER.Clients.RegistryPortal/ecer.clients.registryportal.client/src/components/ECEFiveYearLaborMobilityRequirements.vue index 60bd2d09c..879dea8be 100644 --- a/src/ECER.Clients.RegistryPortal/ecer.clients.registryportal.client/src/components/ECEFiveYearLaborMobilityRequirements.vue +++ b/src/ECER.Clients.RegistryPortal/ecer.clients.registryportal.client/src/components/ECEFiveYearLaborMobilityRequirements.vue @@ -1,14 +1,17 @@ diff --git a/src/ECER.Clients.RegistryPortal/ecer.clients.registryportal.client/src/components/ECESneAndIteLabourMobilityRequirements.vue b/src/ECER.Clients.RegistryPortal/ecer.clients.registryportal.client/src/components/ECESneAndIteLabourMobilityRequirements.vue new file mode 100644 index 000000000..6778c786b --- /dev/null +++ b/src/ECER.Clients.RegistryPortal/ecer.clients.registryportal.client/src/components/ECESneAndIteLabourMobilityRequirements.vue @@ -0,0 +1,81 @@ + + + diff --git a/src/ECER.Clients.RegistryPortal/ecer.clients.registryportal.client/src/components/ECESneLabourMobilityRequirements.vue b/src/ECER.Clients.RegistryPortal/ecer.clients.registryportal.client/src/components/ECESneLabourMobilityRequirements.vue new file mode 100644 index 000000000..f4d53ebe0 --- /dev/null +++ b/src/ECER.Clients.RegistryPortal/ecer.clients.registryportal.client/src/components/ECESneLabourMobilityRequirements.vue @@ -0,0 +1,69 @@ + + + diff --git a/src/ECER.Clients.RegistryPortal/ecer.clients.registryportal.client/src/components/pages/Transfer.stories.ts b/src/ECER.Clients.RegistryPortal/ecer.clients.registryportal.client/src/components/pages/Transfer.stories.ts new file mode 100644 index 000000000..4831e3921 --- /dev/null +++ b/src/ECER.Clients.RegistryPortal/ecer.clients.registryportal.client/src/components/pages/Transfer.stories.ts @@ -0,0 +1,414 @@ +import type { Meta, StoryObj } from "@storybook/vue3-vite"; +import { mocked } from "storybook/test"; +import { getCertificationComparisonList } from "@/api/configuration"; + +import Transfer from "./Transfer.vue"; + +import { getDateMinusYears, getTodayDate } from "@/utils/dateHelpers"; +import { useConfigStore } from "@/store/config"; +import { useCertificationStore } from "@/store/certification.ts"; +import type { Components } from "@/types/openapi.js"; +import type { CertificationLevelType } from "@/types/certificationLevelType.js"; + +// More on how to set up stories at: https://storybook.js.org/docs/writing-stories +const meta = { + title: "Example/Transfer", + component: Transfer, + tags: ["!autodocs"], //Do not generate auto docs for this component. Seems to mess up due to Breadcrumbs.vue + beforeEach: () => { + mocked(getCertificationComparisonList).mockImplementation( + async (provinceId) => { + switch (provinceId) { + case "NONE": + return []; + case "HAS_ALL": + return [ + { + transferringCertificate: { + id: "37e6d6af-d084-ef11-ac21-6045bdcd6cb0", + certificationType: "One year or Five Year", + }, + options: [ + { + id: "f404aa39-e884-ef11-ac20-6045bdccf571", + bcCertificate: "One Year", + }, + { + id: "a81d3567-e884-ef11-ac20-6045bdccf571", + bcCertificate: "Five Year Certificate", + }, + ], + }, + { + transferringCertificate: { + id: "484ff8c5-d084-ef11-ac21-6045bdcd6cb0", + certificationType: "One year or Five year ITE SNE", + }, + options: [ + { + id: "e1b20a8e-e884-ef11-ac20-6045bdccf571", + bcCertificate: "One Year", + }, + { + id: "d6053ca4-e884-ef11-ac20-6045bdccf571", + bcCertificate: "Five Year Certificate+ITE+SNE", + }, + ], + }, + { + transferringCertificate: { + id: "f1efbd35-cd84-ef11-ac21-6045bdcd6cb0", + certificationType: "Assistant", + }, + options: [ + { + id: "e344d10b-e184-ef11-ac20-7c1e524093cc", + bcCertificate: "Assistant", + }, + ], + }, + ]; + default: + return []; + } + }, + ); + }, + decorators: [ + (story) => { + const config = useConfigStore(); + config.provinceList = [ + { provinceId: "NONE", provinceName: "NONE", provinceCode: "NONE" }, + { + provinceId: "HAS_ALL", + provinceName: "Has All", + provinceCode: "HAS_ALL", + }, + ] as Components.Schemas.Province[]; + return story(); + }, + ], +} satisfies Meta; + +export default meta; +type Story = StoryObj; +/* + *👇 Render functions are a framework specific feature to allow you control on how the component renders. + * See https://storybook.js.org/docs/api/csf + * to learn how to use render functions. + */ + +// Assistant - no +// work experience question - yes +// One Year - yes +// Five Year - yes +// Five year ite + sne - yes +export const TransferUserHasNoCertifications: Story = {}; + +export const TransferUserHasAssistant: Story = { + decorators: [ + (story) => { + const certificationStore = useCertificationStore(); + certificationStore.certifications = [ + generateCertificate(["Assistant"], getTodayDate(), "Active"), + ]; + return story(); + }, + ], +}; + +// Assistant - no +// work experience question - no +// One Year - no +// Five Year - yes +// Five year ite + sne - yes +export const TransferUserHasAssistantAndOneYear: Story = { + decorators: [ + (story) => { + const certificationStore = useCertificationStore(); + certificationStore.certifications = [ + generateCertificate(["Assistant"], getTodayDate(), "Active"), + generateCertificate(["ECE 1 YR"], getTodayDate(), "Active"), + ]; + return story(); + }, + ], +}; + +// Assistant - no +// work experience question - no +// One Year - no +// Five Year - no +// Five year ite + sne - no +export const TransferUserFullyCertified: Story = { + decorators: [ + (story) => { + const certificationStore = useCertificationStore(); + certificationStore.certifications = [ + generateCertificate(["Assistant"], getTodayDate(), "Active"), + generateCertificate(["ECE 1 YR"], getTodayDate(), "Active"), + generateCertificate( + ["ECE 5 YR", "ITE", "SNE"], + getDateMinusYears(1), + "Active", + ), + ]; + return story(); + }, + ], +}; + +// Assistant - yes +// work experience question - no +// One Year - no +// Five Year - yes +// Five year ite + sne - yes +export const TransferUserHasOneYear: Story = { + decorators: [ + (story) => { + const certificationStore = useCertificationStore(); + certificationStore.certifications = [ + generateCertificate(["ECE 1 YR"], getTodayDate(), "Active"), + ]; + return story(); + }, + ], +}; + +// Assistant - yes +// work experience question - no +// One Year - no +// Five Year - no +// Five year ite + sne - upgradable +export const TransferUserHasFiveYearShouldBeAbleToUpgradeNoOneYear: Story = { + decorators: [ + (story) => { + const certificationStore = useCertificationStore(); + certificationStore.certifications = [ + generateCertificate(["ECE 5 YR"], getTodayDate(), "Active"), + ]; + return story(); + }, + ], +}; + +// Assistant - yes +// work experience question - no +// One Year - no +// Five Year - no +// Five year ite + sne - upgradable +export const TransferUserHasFiveYearIteShouldBeAbleToUpgrade: Story = { + decorators: [ + (story) => { + const certificationStore = useCertificationStore(); + certificationStore.certifications = [ + generateCertificate(["ECE 5 YR", "ITE"], getTodayDate(), "Active"), + ]; + return story(); + }, + ], +}; + +// Assistant - yes +// work experience question - no +// One Year - no +// Five Year - no +// Five year ite + sne - upgradable +export const TransferUserHasFiveYearSne: Story = { + decorators: [ + (story) => { + const certificationStore = useCertificationStore(); + certificationStore.certifications = [ + generateCertificate(["ECE 5 YR", "SNE"], getTodayDate(), "Active"), + ]; + return story(); + }, + ], +}; + +// Assistant - yes +// work experience question - no +// One Year - no +// Five Year - no +// Five year ite + sne - no +export const TransferUserHasFiveYearIteSne: Story = { + decorators: [ + (story) => { + const certificationStore = useCertificationStore(); + certificationStore.certifications = [ + generateCertificate( + ["ECE 5 YR", "SNE", "ITE"], + getTodayDate(), + "Active", + ), + ]; + return story(); + }, + ], +}; + +// Assistant - yes +// work experience question - no +// One Year - no +// Five Year - no +// Five year ite + sne - upgradable +export const TransferUserHasFiveYearAndOneYearShouldBeAbleToAutoUpgradeToIteSne: Story = + { + decorators: [ + (story) => { + const certificationStore = useCertificationStore(); + certificationStore.certifications = [ + generateCertificate(["ECE 5 YR"], getDateMinusYears(1), "Active"), + generateCertificate(["ECE 1 YR"], getTodayDate(), "Active"), + ]; + return story(); + }, + ], + }; + +// Assistant - yes +// work experience question - no +// One Year - no +// Five Year - no +// Five year ite + sne - upgradable +export const TransferUserHasFiveYearIteAndOneYearShouldBeAbleToAutoUpgradeToSne: Story = + { + decorators: [ + (story) => { + const certificationStore = useCertificationStore(); + certificationStore.certifications = [ + generateCertificate( + ["ECE 5 YR", "ITE"], + getDateMinusYears(1), + "Active", + ), + generateCertificate(["ECE 1 YR"], getTodayDate(), "Active"), + ]; + return story(); + }, + ], + }; + +// Assistant - yes +// work experience question - no +// One Year - yes +// Five Year - no +// Five year ite + sne - no +export const TransferUserHasExpiredFiveYearShouldGetOptionForOneYear: Story = { + decorators: [ + (story) => { + const certificationStore = useCertificationStore(); + certificationStore.certifications = [ + generateCertificate( + ["ECE 5 YR", "SNE", "ITE"], + getDateMinusYears(1), + "Expired", + ), + ]; + return story(); + }, + ], +}; + +// Assistant - yes +// work experience question - no +// One Year - yes +// Five Year - no +// Five year ite + sne - no +export const TransferUserHasExpiredFiveYearAndExpiredOneYearEdgeCaseCanApplyForOneYear: Story = + { + decorators: [ + (story) => { + const certificationStore = useCertificationStore(); + certificationStore.certifications = [ + generateCertificate( + ["ECE 5 YR", "SNE", "ITE"], + getTodayDate(), + "Expired", + ), + generateCertificate(["ECE 1 YR"], getDateMinusYears(1), "Expired"), + ]; + return story(); + }, + ], + }; + +// Assistant - yes +// work experience question - no +// One Year - no +// Five Year - no +// Five year ite + sne - no +export const TransferUserHasActiveFiveYearAndExpiredOneYearEdgeCaseCannotApplyForOneYear: Story = + { + decorators: [ + (story) => { + const certificationStore = useCertificationStore(); + certificationStore.certifications = [ + generateCertificate( + ["ECE 5 YR", "SNE", "ITE"], + getTodayDate(), + "Active", + ), + generateCertificate(["ECE 1 YR"], getDateMinusYears(1), "Expired"), + ]; + return story(); + }, + ], + }; + +// Assistant - yes +// work experience question - no +// One Year - no +// Five Year - no +// Five year ite + sne - no +export const TransferUserHasExpiredFiveYearAndOneYearAfterShouldNotBeAbleToApplyForOneYear: Story = + { + decorators: [ + (story) => { + const certificationStore = useCertificationStore(); + certificationStore.certifications = [ + generateCertificate( + ["ECE 5 YR", "SNE", "ITE"], + getDateMinusYears(1), + "Expired", + ), + generateCertificate(["ECE 1 YR"], getTodayDate(), "Expired"), + ]; + return story(); + }, + ], + }; + +// helper functions to generate certificates +const generateCertificate = ( + levels: CertificationLevelType[], + date: string, + status: "Expired" | "Active", +): Components.Schemas.Certification => { + return { + id: "f403a278-8020-f011-998a-6045bdf9b81b", + name: levels.join(","), + number: "016359", + expiryDate: date, + effectiveDate: date, + date: date, + printDate: null, + hasConditions: false, + levelName: levels.join(","), + statusCode: status, + certificatePDFGeneration: "Yes", + levels: levels.map((level) => { + return { id: "556b387e-8020-f011-998a-7c1e52871876", type: level }; + }), + files: [ + { + id: "3979ff88-f262-4747-b294-c289caa2402a", + url: "ecer_certificate/f403a278-8020-f011-998a-6045bdf9b81b", + extention: ".pdf", + size: "322.00 KB", + name: "Cover Letter-016359.pdf", + }, + ], + certificateConditions: [], + }; +}; diff --git a/src/ECER.Clients.RegistryPortal/ecer.clients.registryportal.client/src/components/pages/Transfer.vue b/src/ECER.Clients.RegistryPortal/ecer.clients.registryportal.client/src/components/pages/Transfer.vue index 2280948f7..34626a9a5 100644 --- a/src/ECER.Clients.RegistryPortal/ecer.clients.registryportal.client/src/components/pages/Transfer.vue +++ b/src/ECER.Clients.RegistryPortal/ecer.clients.registryportal.client/src/components/pages/Transfer.vue @@ -94,21 +94,13 @@ - - - -

- You will be able to work alongside ECEs and/or Infant and - Toddler Educators in licensed child care programs for children - birth to 5 years of age. -

-
-
-
- +

Work experience

@@ -141,8 +133,42 @@ + + + + + + +

+ Your certification from another province transfers to a + certification level you have already held in B.C. You can renew + or + + apply for certification + + on your dashboard through a different pathway. +

+ + +

+ You will be able to work alongside ECEs and/or Infant and + Toddler Educators in licensed child care programs for children + birth to 5 years of age. +

+
@@ -164,9 +190,11 @@ Educator (SNE).

+ + + This certification allows you to to work alone and/or as the + primary educator in licensed child care programs for children + birth to 5 years of age. It will also renew your ECE Five Year + certificate. It is valid for 5 years. + + + This certification allows you to to work alone and/or as the + primary educator in licensed child care programs for children + birth to 5 years of age. It will also renew your ECE Five Year + certificate. It is valid for 5 years. + + + + Specialized certifications allow you to work in licensed child + care programs: + +
+
    +
  • + alone and/or as the primary educator with children birth to 5 + years (ITE) +
  • +
  • + alone and/or as the primary educator in inclusive settings + with children 3-5 years of age (SNE) +
  • +
+
@@ -247,35 +333,39 @@ import { getCertificationComparisonList } from "@/api/configuration"; import Breadcrumb from "@/components/Breadcrumb.vue"; import PageContainer from "@/components/PageContainer.vue"; -import ProfileForm from "@/components/ProfileForm.vue"; import { getHighestCertificationType, CertificationType, } from "@/utils/functions"; import { useConfigStore } from "@/store/config"; import { useApplicationStore } from "@/store/application"; +import { useCertificationStore } from "@/store/certification"; import { useRouter } from "vue-router"; import * as Rules from "@/utils/formRules"; import type { Components, Province, ComparisonRecord } from "@/types/openapi"; import Callout from "@/components/Callout.vue"; import { useUserStore } from "@/store/user"; +import { sortBy } from "lodash"; + interface EceTransferData { province: Province | undefined; outOfProvinceCertification: ComparisonRecord | undefined; } export default { name: "Transfer", - components: { ProfileForm, Breadcrumb, PageContainer, Callout }, + components: { Breadcrumb, PageContainer, Callout }, setup() { const configStore = useConfigStore(); const router = useRouter(); const applicationStore = useApplicationStore(); const userStore = useUserStore(); + const certificationStore = useCertificationStore(); return { configStore, router, applicationStore, userStore, + certificationStore, }; }, methods: { @@ -284,11 +374,15 @@ export default { this.outOfProvinceCertification = undefined; this.has500HoursWorkExperience = undefined; this.highestCertificationType = undefined; + this.filteredCertificationOptions = undefined; if (this.transferData.province?.provinceId) { this.outOfProvinceCertificationTypes = - (await getCertificationComparisonList( - this.transferData.province?.provinceId, - )) || []; + sortBy( + await getCertificationComparisonList( + this.transferData.province?.provinceId, + ), + ["transferringCertificate.certificationType"], + ) || []; this.outOfProvinceCertificationTypesLoading = false; } else { this.outOfProvinceCertificationTypes = []; @@ -298,9 +392,80 @@ export default { async onCertificationChange() { this.highestCertificationType = undefined; this.has500HoursWorkExperience = undefined; + if (this.transferData.outOfProvinceCertification?.options) { + //start filtering based on a user's existing certifications + this.filteredCertificationOptions = + this.transferData.outOfProvinceCertification?.options.filter( + (option) => { + // Five year certification level restrictions + if ( + this.activeFiveYearCertification && + option.bcCertificate === CertificationType.FiveYearCertificate + ) { + return false; + } + + if ( + this.activeFullyCertifiedFiveYearCertification && + option.bcCertificate === + CertificationType.FiveYearCertificateITE_SNE + ) { + return false; + } + + if ( + this.certificationStore.hasExpiredCertificationsOfType( + "ECE 5 YR", + ) && + (option.bcCertificate === + CertificationType.FiveYearCertificate || + option.bcCertificate === + CertificationType.FiveYearCertificateITE_SNE) + ) { + return false; + } + + //one year rules + //active one year certification should renew instead + if ( + this.certificationStore.activeEceOneYearCertification && + option.bcCertificate === CertificationType.OneYear + ) { + return false; + } + + //not allowed to apply for a new one year certificate without a 5 year cert reset + if ( + this.certificationStore + .countMultipleEceOneYearCertificationsSinceLatest5YearIfExist > + 0 && + option.bcCertificate === CertificationType.OneYear + ) { + return false; + } + + if ( + this.certificationStore.activeEceFiveYearCertification && + option.bcCertificate === CertificationType.OneYear + ) { + return false; + } + + //assistant rules + if ( + this.certificationStore.hasCertificationsOfType("Assistant") && + option.bcCertificate === CertificationType.Assistant + ) { + return false; + } + + return true; + }, + ); + this.highestCertificationType = getHighestCertificationType( - this.transferData.outOfProvinceCertification?.options, + this.filteredCertificationOptions, ); } }, @@ -341,6 +506,11 @@ export default { this.selfAssessmentOutcome.includes("Sne") ) { certificationType = CertificationType.FiveYearCertificateITE_SNE; + } else if ( + this.selfAssessmentOutcome.includes("Ite") || + this.selfAssessmentOutcome.includes("Sne") + ) { + certificationType = CertificationType.FiveYearCertificateITE_SNE; } else if (this.selfAssessmentOutcome.includes("FiveYears")) { certificationType = CertificationType.FiveYearCertificate; } else if (this.selfAssessmentOutcome.includes("OneYear")) { @@ -361,6 +531,61 @@ export default { }, }, computed: { + activeFiveYearCertification(): Components.Schemas.Certification | null { + return this.certificationStore.activeEceFiveYearCertification; + }, + activeFullyCertifiedFiveYearCertification(): boolean | undefined { + if (!this.activeFiveYearCertification) { + return false; + } + + return ( + this.activeFiveYearCertification.levels?.some( + (level) => level.type === "ITE", + ) && + this.activeFiveYearCertification.levels?.some( + (level) => level.type === "SNE", + ) + ); + }, + remainingPostBasic(): "ITE" | "SNE" | "ITE_AND_SNE" | undefined { + if (!this.activeFiveYearCertification) { + return undefined; + } + + if ( + this.activeFiveYearCertification.levels?.some( + (level) => level.type === "ITE", + ) && + this.activeFiveYearCertification.levels?.some( + (level) => level.type === "SNE", + ) + ) { + console.warn("user is fully certified why do they need to do LM?"); + return undefined; + } else if ( + !this.activeFiveYearCertification.levels?.some( + (level) => level.type === "ITE", + ) && + !this.activeFiveYearCertification.levels?.some( + (level) => level.type === "SNE", + ) + ) { + return "ITE_AND_SNE"; + } else if ( + !this.activeFiveYearCertification.levels?.some( + (level) => level.type === "ITE", + ) + ) { + return "ITE"; + } else if ( + !this.activeFiveYearCertification.levels?.some( + (level) => level.type === "SNE", + ) + ) { + return "SNE"; + } + }, transferData(): EceTransferData { return { province: this.province, @@ -385,23 +610,48 @@ export default { if ( this.highestCertificationType === CertificationType.FiveYearCertificate && - this.has500HoursWorkExperience === "true" + (this.has500HoursWorkExperience === "true" || + (this.filteredCertificationOptions?.length === 1 && + !this.certificationStore.holdsEceFiveYearCertification)) ) { return ["FiveYears"]; } if ( this.highestCertificationType === CertificationType.FiveYearCertificateITE_SNE && - this.has500HoursWorkExperience === "true" + (this.has500HoursWorkExperience === "true" || + (this.filteredCertificationOptions?.length === 1 && + !this.certificationStore.holdsEceFiveYearCertification)) ) { return ["FiveYears", "Ite", "Sne"]; } + //flow for those that have an active 5 year certificate and want to upgrade + if ( + this.highestCertificationType === + CertificationType.FiveYearCertificateITE_SNE && + this.remainingPostBasic + ) { + if (this.remainingPostBasic === "ITE") { + return ["Ite"]; + } + if (this.remainingPostBasic === "SNE") { + return ["Sne"]; + } + if (this.remainingPostBasic === "ITE_AND_SNE") { + return ["Ite", "Sne"]; + } + } + console.warn( + "Unable to assign specific certificate type. This should not happen.", + ); return []; }, showRequirementsButton() { return ( this.highestCertificationType === CertificationType.Assistant || - this.has500HoursWorkExperience + this.highestCertificationType === CertificationType.OneYear || + this.has500HoursWorkExperience || + this.filteredCertificationOptions?.length === 1 ); }, }, @@ -409,6 +659,8 @@ export default { outOfProvinceCertificationTypesLoading: false, has500HoursWorkExperience: undefined, highestCertificationType: undefined as CertificationType | undefined, + filteredCertificationOptions: undefined as + Components.Schemas.CertificationComparison[] | undefined, outOfProvinceCertificationTypes: [] as Components.Schemas.ComparisonRecord[], Rules, diff --git a/src/ECER.Clients.RegistryPortal/ecer.clients.registryportal.client/src/config/application-wizard-labour-mobility-ite-sne.ts b/src/ECER.Clients.RegistryPortal/ecer.clients.registryportal.client/src/config/application-wizard-labour-mobility-ite-sne.ts new file mode 100644 index 000000000..fd19f3acf --- /dev/null +++ b/src/ECER.Clients.RegistryPortal/ecer.clients.registryportal.client/src/config/application-wizard-labour-mobility-ite-sne.ts @@ -0,0 +1,38 @@ +import type { Wizard } from "@/types/wizard"; + +import characterReferencesForm from "./character-references-form"; +import previewFormIteSneLaborMobility from "./preview-form-ite-sne-labour-mobility"; +import profileInformationForm from "./profile-information-form"; +import certificateInformationForm from "./certificate-information-form"; + +const applicationWizard: Wizard = { + id: "form-1", + steps: { + certificateInformation: { + stage: "CertificateInformation", + title: "Certificate information", + form: certificateInformationForm, + key: "item.1", + }, + profile: { + stage: "ContactInformation", + title: "Contact information", + form: profileInformationForm, + key: "item.2", + }, + characterReferences: { + stage: "CharacterReferences", + title: "Character reference", + form: characterReferencesForm, + key: "item.3", + }, + review: { + stage: "Review", + title: "Review and submit", + form: previewFormIteSneLaborMobility, + key: "item.5", + }, + }, +}; + +export default applicationWizard; diff --git a/src/ECER.Clients.RegistryPortal/ecer.clients.registryportal.client/src/config/preview-form-ite-sne-labour-mobility.ts b/src/ECER.Clients.RegistryPortal/ecer.clients.registryportal.client/src/config/preview-form-ite-sne-labour-mobility.ts new file mode 100644 index 000000000..1c81f8aaf --- /dev/null +++ b/src/ECER.Clients.RegistryPortal/ecer.clients.registryportal.client/src/config/preview-form-ite-sne-labour-mobility.ts @@ -0,0 +1,41 @@ +import EceCertificateInformationPreview from "@/components/inputs/EceCertificateInformationPreview.vue"; +import EceCharacterReferencePreview from "@/components/inputs/EceCharacterReferencePreview.vue"; +import EceContactInformationPreview from "@/components/inputs/EceContactInformationPreview.vue"; + +import type { Form } from "@/types/form"; + +const previewForm: Form = { + id: "previewForm", + title: "Review", + inputs: { + certificateInformationPreview: { + id: "certificateInformationPreview", + component: EceCertificateInformationPreview, + cols: { + md: 12, + lg: 12, + xl: 12, + }, + }, + contactInformationPreview: { + id: "contactInformationPreview", + component: EceContactInformationPreview, + cols: { + md: 12, + lg: 12, + xl: 12, + }, + }, + characterReferencePreview: { + id: "characterReferencePreview", + component: EceCharacterReferencePreview, + cols: { + md: 12, + lg: 12, + xl: 12, + }, + }, + }, +}; + +export default previewForm; diff --git a/src/ECER.Clients.RegistryPortal/ecer.clients.registryportal.client/src/store/application.ts b/src/ECER.Clients.RegistryPortal/ecer.clients.registryportal.client/src/store/application.ts index 0596d4a0b..4a5cc81d6 100644 --- a/src/ECER.Clients.RegistryPortal/ecer.clients.registryportal.client/src/store/application.ts +++ b/src/ECER.Clients.RegistryPortal/ecer.clients.registryportal.client/src/store/application.ts @@ -18,7 +18,9 @@ import applicationWizardRenewOneYearActive from "@/config/application-wizard-ren import applicationWizardRenewOneYearExpired from "@/config/application-wizard-renew-one-year-expired"; import applicationWizardLaborMobilityAssistantAndOneYear from "@/config/application-wizard-labor-mobility-assistant-and-one-year"; import applicationWizardLaborMobilityFiveYear from "@/config/application-wizard-labor-mobility-five-year"; +import applicationWizardLabourMobilityIteSne from "@/config/application-wizard-labour-mobility-ite-sne"; import applicationWizardIcraFiveYear from "@/config/icra-eligibility/application-wizard-icra-five-year"; + import type { Components } from "@/types/openapi"; import type { ApplicationStage, Wizard } from "@/types/wizard"; import { expiredMoreThan5Years, humanFileSize } from "@/utils/functions"; @@ -43,6 +45,9 @@ export type ApplicationFlow = | "AssistantLaborMobility" | "OneYearLaborMobility" | "FiveYearLaborMobility" + | "SneLabourMobility" + | "IteLabourMobility" + | "IteAndSneLabourMobility" | "AssistantRenewal" | "OneYearActiveRenewal" | "OneYearExpiredRenewal" @@ -187,6 +192,10 @@ export const useApplicationStore = defineStore("application", { case "SneRegistrant": case "IteAndSneRegistrant": return applicationWizardIteSne; + case "IteLabourMobility": + case "SneLabourMobility": + case "IteAndSneLabourMobility": + return applicationWizardLabourMobilityIteSne; case "AssistantRenewal": return applicationWizardRenewAssistant; case "OneYearActiveRenewal": @@ -260,6 +269,10 @@ export const useApplicationStore = defineStore("application", { if (state.draftApplication.applicationType === "LabourMobility") { if (this.isDraftCertificateTypeFiveYears) return "FiveYearLaborMobility"; + if (this.isDraftCertificateTypeIte && this.isDraftCertificateTypeSne) + return "IteAndSneLabourMobility"; + if (this.isDraftCertificateTypeIte) return "IteLabourMobility"; + if (this.isDraftCertificateTypeSne) return "SneLabourMobility"; if (this.isDraftCertificateTypeOneYear) return "OneYearLaborMobility"; if (this.isDraftCertificateTypeEceAssistant) return "AssistantLaborMobility"; diff --git a/src/ECER.Clients.RegistryPortal/ecer.clients.registryportal.client/src/store/certification.ts b/src/ECER.Clients.RegistryPortal/ecer.clients.registryportal.client/src/store/certification.ts index 14d92c410..84844eb80 100644 --- a/src/ECER.Clients.RegistryPortal/ecer.clients.registryportal.client/src/store/certification.ts +++ b/src/ECER.Clients.RegistryPortal/ecer.clients.registryportal.client/src/store/certification.ts @@ -42,6 +42,18 @@ export const useCertificationStore = defineStore("certification", { ) ?? null ); }, + activeEceOneYearCertification( + state, + ): Components.Schemas.Certification | null { + return ( + state.certifications?.find( + (cert) => + cert.levels?.some( + (level) => level.type === ("ECE 1 YR" as CertificationLevelType), + ) && cert.statusCode === "Active", + ) ?? null + ); + }, holdsAllCertifications(state): boolean { if (!state.certifications || state.certifications.length === 0) return false; @@ -196,6 +208,24 @@ export const useCertificationStore = defineStore("certification", { 0 ); }, + hasExpiredCertificationsOfType( + certificateLevelType: CertificationLevelType, + ): boolean { + if (!this.certifications) return false; + return this.certifications.some( + (cert) => + cert.statusCode === "Expired" && + cert.levels?.some((level) => level.type === certificateLevelType), + ); + }, + hasCertificationsOfType( + certificateLevelType: CertificationLevelType, + ): boolean { + if (!this.certifications) return false; + return this.certifications.some((cert) => + cert.levels?.some((level) => level.type === certificateLevelType), + ); + }, getMostRecentCertificationByExpiryDate( certificateType: CertificationLevelType, ): Components.Schemas.Certification | undefined { diff --git a/src/ECER.Clients.RegistryPortal/ecer.clients.registryportal.client/src/utils/functions.ts b/src/ECER.Clients.RegistryPortal/ecer.clients.registryportal.client/src/utils/functions.ts index 668d1bd25..208c329f4 100644 --- a/src/ECER.Clients.RegistryPortal/ecer.clients.registryportal.client/src/utils/functions.ts +++ b/src/ECER.Clients.RegistryPortal/ecer.clients.registryportal.client/src/utils/functions.ts @@ -282,8 +282,7 @@ export function parseCertificationType(input: string): CertificationType { * Returns the highest certification name from a list of options. * * @param {CertificationComparison[]} options - An array of certification comparison objects. - * @returns {string} - The highest certification name based on the defined weights. - * @throws {Error} - If no valid certification type is found in the options. + * @returns {string | undefined} - The highest certification name based on the defined weights. */ export function getHighestCertificationType( options: CertificationComparison[], @@ -293,7 +292,7 @@ export function getHighestCertificationType( .filter((c): c is CertificationType => !!c); // drop unrecognized if (parsed.length === 0) { - throw new Error("No valid certification found in options"); + return undefined; } return parsed.reduce( diff --git a/src/ECER.Infrastructure.Common/Validators/RouterValidationExtensions.cs b/src/ECER.Infrastructure.Common/Validators/RouterValidationExtensions.cs index f53bfa658..5b9966aae 100644 --- a/src/ECER.Infrastructure.Common/Validators/RouterValidationExtensions.cs +++ b/src/ECER.Infrastructure.Common/Validators/RouterValidationExtensions.cs @@ -34,13 +34,14 @@ public static RouteHandlerBuilder AddGuidValidationQueryParams(this RouteHandler foreach (var queryParameter in parameterNames) { var value = context.HttpContext.Request.Query[queryParameter].ToString(); + bool valueProvided = context.HttpContext.Request.Query.TryGetValue(queryParameter, out var _); - if (string.IsNullOrWhiteSpace(value) && isRequired) + if (!valueProvided && isRequired) { - errors.Add($"{queryParameter} is required cannot be null or whitespace"); + errors.Add($"{queryParameter} is required"); } - if (!string.IsNullOrEmpty(value) && !Guid.TryParse(value, out _)) + if (valueProvided && !Guid.TryParse(value, out _)) { errors.Add($"{queryParameter} must be a valid GUID"); }