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 @@
-
+
You need the following information to apply to transfer your
- certification to B.C.
+ certification to British Columbia.
@@ -97,26 +100,16 @@
-
-
-
-
-
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 @@
+
+
+
+
+ Transfer to Special Needs Educator and Infant and Toddler Educator
+ certification
+
+
+ This certification adds specializations to your ECE Five Year
+ certificate that allow you to work in licensed child care programs:
+
+
+ -
+ alone and/or as the primary educator in inclusive settings with
+ children 3-5 years of age (SNE)
+
+ -
+ alone and/or as the primary educator with children birth to 5 years
+ (ITE)
+
+
+
+ You need the following information to apply to transfer your
+ certification to British Columbia.
+
+
+
+
+
+
+
+
You will need to provide:
+
+ -
+ Your certification or registration number (if applicable to your
+ province)
+
+ - The name on your certificate
+
+
+
+
+
+
+
+
+
+ You will need to provide a character reference. You will enter their
+ name and email. We will contact them later after you submit your
+ application.
+
+
+ Make sure you choose a person that:
+
+
+ - Can speak to your character
+ -
+ Can speak to your ability to educate and care for young children
+
+ - Has known you for at least 6 months
+ - Is not your relative, partner, spouse or yourself
+
+
+
+ We recommend the person is a certified ECE who has directly observed you
+ working with young children.
+
+
+
+
+
+
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 @@
+
+
+
+ Transfer to Special Needs Educator certification
+
+ This certificate allows you to work alone and/or as the primary educator
+ in inclusive, licensed child care programs for children 3 to 5 years of
+ age. It will also renew your ECE Five Year certificate. It is valid for
+ 5 years.
+
+
+ You need the following information to apply to transfer your
+ certification to British Columbia.
+
+
+
+
+
+
+
+
You will need to provide:
+
+ -
+ Your certification or registration number (if applicable to your
+ province)
+
+ - The name on your certificate
+
+
+
+
+
+
+
+
+ You will need to provide a character reference. You will enter their
+ name and email. We will contact them later after you submit your
+ application.
+
+
+ Make sure you choose a person that:
+
+
+ - Can speak to your character
+ -
+ Can speak to your ability to educate and care for young children
+
+ - Has known you for at least 6 months
+ - Is not your relative, partner, spouse or yourself
+
+
+
+ We recommend the person is a certified ECE who has directly observed you
+ working with young children.
+
+
+
+
+
+
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");
}