Repository: bcgov/SIMS
Pull request #4526: #4431 - Student View change request revamp - View Application Versions
Author: login: andrewsignori-aot
Labels: name: Student
description: Student Features
name: SIMS-Api
description: SIMS-Api
name: Form.io
description: Form IO definitions changed.
name: Web portal
Created: 2025-03-24T21:13:30Z | Merged: 2025-03-31T19:13:52Z

## Student Applications Summary Changes

- New `View` button added with the same link from "Financial Aid Application";
- New `Change request` button redirecting to the current "Request a Change" for program years before 2025-2026.  __Note:_ This logic was implemented in the API to allow server validation in the future. It is considered temporary till all program years support the new feature and it was implemented with simplicity in mind._
  - The current `Request a change` option inside the application details always redirects to the current appeals feature. It will be changed in an upcoming PR.
- New `Versions` expander to allow the visualization of the previously submitted applications. Its content is currently loaded on demand.

![image](https://github.com/user-attachments/assets/f1ff4fb2-ca43-4e54-a741-6fca5354e2ef)

## Request a Change Page Changes

- Navigation menu removed from the top nav bar.
- Application ID is provided in the URL and the application number now rendered as a read-only text input.
_Note:_ the destiny of this page and this feature as it is, in under review. No further time will be invested and the goal is just to keep it working for now.

![image](https://github.com/user-attachments/assets/709cf249-7116-41de-9c2d-99da85ff40e0)

![image](https://github.com/user-attachments/assets/3be2a18d-f4ab-4f09-9715-1a99a397c861)

## Non-related changes/suggestions

As part of this PR, it was introduced the [skeleton-loaders](https://vuetifyjs.com/en/components/skeleton-loaders/#usage) with the intention to start using them along the solution. 

![image](https://github.com/user-attachments/assets/d93a4556-f1c1-4ad3-ae61-72c50fe0dc01)


Diff:
diff --git a/sources/packages/backend/apps/api/src/constants/error-code.constants.ts b/sources/packages/backend/apps/api/src/constants/error-code.constants.ts
index 55f8b29cb1..3492aa8991 100644
--- a/sources/packages/backend/apps/api/src/constants/error-code.constants.ts
+++ b/sources/packages/backend/apps/api/src/constants/error-code.constants.ts
@@ -5,7 +5,6 @@ export const MISSING_STUDENT_ACCOUNT = "MISSING_STUDENT_ACCOUNT";
 export const MISSING_USER_ACCOUNT = "MISSING_USER_ACCOUNT";
 export const MISSING_GROUP_ACCESS = "MISSING_GROUP_ACCESS";
 export const MISSING_USER_INFO = "MISSING_USER_INFO";
-export const INVALID_APPLICATION_NUMBER = "INVALID_APPLICATION_NUMBER";
 /**
  * An application can have only one pending appeal at a time.
  */
diff --git a/sources/packages/backend/apps/api/src/route-controllers/application/_tests_/application.aest.controller.getApplicationOverallDetails.e2e-spec.ts b/sources/packages/backend/apps/api/src/route-controllers/application/_tests_/application.aest.controller.getApplicationOverallDetails.e2e-spec.ts
index aa43f4e609..7e7461f0c5 100644
--- a/sources/packages/backend/apps/api/src/route-controllers/application/_tests_/application.aest.controller.getApplicationOverallDetails.e2e-spec.ts
+++ b/sources/packages/backend/apps/api/src/route-controllers/application/_tests_/application.aest.controller.getApplicationOverallDetails.e2e-spec.ts
@@ -79,10 +79,14 @@ describe("ApplicationAESTController(e2e)-getApplicationOverallDetails", () => {
           {
             id: secondVersionApplication.id,
             submittedDate: secondVersionApplication.submittedDate.toISOString(),
+            applicationEditStatus:
+              secondVersionApplication.applicationEditStatus,
           },
           {
             id: firstVersionApplication.id,
             submittedDate: firstVersionApplication.submittedDate.toISOString(),
+            applicationEditStatus:
+              firstVersionApplication.applicationEditStatus,
           },
         ],
       });
diff --git a/sources/packages/backend/apps/api/src/route-controllers/application/application.aest.controller.ts b/sources/packages/backend/apps/api/src/route-controllers/application/application.aest.controller.ts
index deebc711ea..59ba4ccb13 100644
--- a/sources/packages/backend/apps/api/src/route-controllers/application/application.aest.controller.ts
+++ b/sources/packages/backend/apps/api/src/route-controllers/application/application.aest.controller.ts
@@ -279,19 +279,8 @@ export class ApplicationAESTController extends BaseController {
   async getApplicationOverallDetails(
     @Param("applicationId", ParseIntPipe) applicationId: number,
   ): Promise<ApplicationOverallDetailsAPIOutDTO> {
-    const application = await this.applicationService.doesApplicationExist({
-      applicationId: applicationId,
-    });
-    if (!application) {
-      throw new NotFoundException("Application not found.");
-    }
-    const applications =
-      await this.applicationService.getAllApplicationVersions(applicationId);
-    return {
-      previousVersions: applications.map((application) => ({
-        id: application.id,
-        submittedDate: application.submittedDate,
-      })),
-    };
+    return this.applicationControllerService.getApplicationOverallDetails(
+      applicationId,
+    );
   }
 }
diff --git a/sources/packages/backend/apps/api/src/route-controllers/application/application.controller.service.ts b/sources/packages/backend/apps/api/src/route-controllers/application/application.controller.service.ts
index 4dece3e9c4..821fed09d4 100644
--- a/sources/packages/backend/apps/api/src/route-controllers/application/application.controller.service.ts
+++ b/sources/packages/backend/apps/api/src/route-controllers/application/application.controller.service.ts
@@ -14,6 +14,7 @@ import {
   ApplicationOfferingChangeRequestService,
   CRAIncomeVerificationService,
   SupportingUserService,
+  APPLICATION_NOT_FOUND,
 } from "../../services";
 import {
   ApplicationFormData,
@@ -27,6 +28,7 @@ import {
   CompletedApplicationDetailsAPIOutDTO,
   InProgressApplicationDetailsAPIOutDTO,
   ApplicationDataChangeAPIOutDTO,
+  ApplicationOverallDetailsAPIOutDTO,
 } from "./models/application.dto";
 import {
   credentialTypeToDisplay,
@@ -40,6 +42,7 @@ import {
   ApplicationDataChange,
   getDateOnlyFormat,
   compareApplicationData,
+  CustomNamedError,
 } from "@sims/utilities";
 import {
   Application,
@@ -736,4 +739,38 @@ export class ApplicationControllerService {
 
     return supportingUserDetails;
   }
+
+  /**
+   * Get application overall details for the given application.
+   * @param applicationId application ID.
+   * @param options options.
+   * - `studentId` student ID used for authorization.
+   * @returns application overall details.
+   */
+  async getApplicationOverallDetails(
+    applicationId: number,
+    options?: { studentId?: number },
+  ): Promise<ApplicationOverallDetailsAPIOutDTO> {
+    try {
+      const applications =
+        await this.applicationService.getAllApplicationVersions(applicationId, {
+          studentId: options?.studentId,
+        });
+      return {
+        previousVersions: applications.map((application) => ({
+          id: application.id,
+          submittedDate: application.submittedDate,
+          applicationEditStatus: application.applicationEditStatus,
+        })),
+      };
+    } catch (error: unknown) {
+      if (
+        error instanceof CustomNamedError &&
+        error.name === APPLICATION_NOT_FOUND
+      ) {
+        throw new NotFoundException("Application not found.");
+      }
+      throw error;
+    }
+  }
 }
diff --git a/sources/packages/backend/apps/api/src/route-controllers/application/application.students.controller.ts b/sources/packages/backend/apps/api/src/route-controllers/application/application.students.controller.ts
index 0b1ebbbfd8..196eb486fc 100644
--- a/sources/packages/backend/apps/api/src/route-controllers/application/application.students.controller.ts
+++ b/sources/packages/backend/apps/api/src/route-controllers/application/application.students.controller.ts
@@ -34,12 +34,12 @@ import {
   ApplicationDataAPIOutDTO,
   ApplicationWithProgramYearAPIOutDTO,
   ApplicationProgramYearAPIOutDTO,
-  ApplicationNumberParamAPIInDTO,
   InProgressApplicationDetailsAPIOutDTO,
   ApplicationProgressDetailsAPIOutDTO,
   EnrolmentApplicationDetailsAPIOutDTO,
   CompletedApplicationDetailsAPIOutDTO,
   ApplicationWarningsAPIOutDTO,
+  ApplicationOverallDetailsAPIOutDTO,
 } from "./models/application.dto";
 import {
   AllowAuthorizedParty,
@@ -52,7 +52,6 @@ import { ApiProcessError, ClientTypeBaseRoute } from "../../types";
 import { STUDY_DATE_OVERLAP_ERROR } from "../../utilities";
 import {
   INSTITUTION_LOCATION_NOT_VALID,
-  INVALID_APPLICATION_NUMBER,
   OFFERING_NOT_VALID,
 } from "../../constants";
 import {
@@ -105,7 +104,11 @@ export class ApplicationStudentsController extends BaseController {
   ): Promise<ApplicationDataAPIOutDTO> {
     const application = await this.applicationService.getApplicationById(
       applicationId,
-      { loadDynamicData: true, studentId: userToken.studentId },
+      {
+        loadDynamicData: true,
+        studentId: userToken.studentId,
+        allowEdited: true,
+      },
     );
     if (!application) {
       throw new NotFoundException(
@@ -547,33 +550,27 @@ export class ApplicationStudentsController extends BaseController {
   }
 
   /**
-   * Get application to request appeal.
-   ** Application eligible to be requested for
-   ** a change will be returned.
-   * @param applicationNumber
-   * @param userToken
-   * @returns application
+   * Get application to request an appeal.
+   * @param applicationId application ID.
+   * @returns application eligible to be requested for a change.
    */
   @ApiNotFoundResponse({
     description:
       "Application either not found or not eligible to request for change.",
   })
-  @Get(":applicationNumber/appeal")
+  @Get(":applicationId/appeal")
   async getApplicationToRequestAppeal(
-    @Param() applicationNumberParam: ApplicationNumberParamAPIInDTO,
+    @Param("applicationId", ParseIntPipe) applicationId: number,
     @UserToken() userToken: IUserToken,
   ): Promise<ApplicationProgramYearAPIOutDTO> {
     const application =
       await this.applicationService.getApplicationToRequestAppeal(
+        applicationId,
         userToken.userId,
-        applicationNumberParam.applicationNumber,
       );
     if (!application) {
       throw new NotFoundException(
-        new ApiProcessError(
-          "Given application either does not exist or is not complete to request change.",
-          INVALID_APPLICATION_NUMBER,
-        ),
+        "Given application either does not exist or is not complete to request change.",
       );
     }
     return {
@@ -664,4 +661,23 @@ export class ApplicationStudentsController extends BaseController {
       },
     );
   }
+
+  /**
+   * Get application overall details for the given application.
+   * @param applicationId application Id.
+   * @returns application overall details.
+   */
+  @ApiNotFoundResponse({
+    description: "Application not found.",
+  })
+  @Get(":applicationId/overall-details")
+  async getApplicationOverallDetails(
+    @Param("applicationId", ParseIntPipe) applicationId: number,
+    @UserToken() userToken: StudentUserToken,
+  ): Promise<ApplicationOverallDetailsAPIOutDTO> {
+    return this.applicationControllerService.getApplicationOverallDetails(
+      applicationId,
+      { studentId: userToken.studentId },
+    );
+  }
 }
diff --git a/sources/packages/backend/apps/api/src/route-controllers/application/models/application.dto.ts b/sources/packages/backend/apps/api/src/route-controllers/application/models/application.dto.ts
index 5f4e3fe569..e327ddc315 100644
--- a/sources/packages/backend/apps/api/src/route-controllers/application/models/application.dto.ts
+++ b/sources/packages/backend/apps/api/src/route-controllers/application/models/application.dto.ts
@@ -1,5 +1,5 @@
 import { IntersectionType } from "@nestjs/swagger";
-import { IsObject, IsOptional, IsPositive, Length } from "class-validator";
+import { IsObject, IsOptional, IsPositive } from "class-validator";
 import {
   ApplicationExceptionStatus,
   ApplicationStatus,
@@ -8,13 +8,13 @@ import {
   COEStatus,
   ApplicationData,
   OfferingIntensity,
-  APPLICATION_NUMBER_LENGTH,
   DisbursementScheduleStatus,
   StudentAppealStatus,
   AssessmentTriggerType,
   StudentScholasticStandingChangeType,
   ApplicationOfferingChangeRequestStatus,
   StudentAssessmentStatus,
+  ApplicationEditStatus,
 } from "@sims/sims-db";
 import { JsonMaxSize } from "../../../utilities/class-validation";
 import { JSON_20KB } from "../../../constants";
@@ -142,11 +142,6 @@ export enum SuccessWaitingStatus {
   Waiting = "Waiting",
 }
 
-export class ApplicationNumberParamAPIInDTO {
-  @Length(APPLICATION_NUMBER_LENGTH, APPLICATION_NUMBER_LENGTH)
-  applicationNumber: string;
-}
-
 export class ApplicationIncomeVerification {
   parent1IncomeVerificationStatus?: SuccessWaitingStatus;
   parent2IncomeVerificationStatus?: SuccessWaitingStatus;
@@ -224,6 +219,7 @@ export class ApplicationWarningsAPIOutDTO {
 export class ApplicationVersionAPIOutDTO {
   id: number;
   submittedDate: Date;
+  applicationEditStatus: ApplicationEditStatus;
 }
 
 export class ApplicationOverallDetailsAPIOutDTO {
diff --git a/sources/packages/backend/apps/api/src/route-controllers/student-appeal/_tests_/e2e/student-appeal.students.controller.submitStudentAppeal.e2e-spec.ts b/sources/packages/backend/apps/api/src/route-controllers/student-appeal/_tests_/e2e/student-appeal.students.controller.submitStudentAppeal.e2e-spec.ts
index aec4689ecc..02d5d26647 100644
--- a/sources/packages/backend/apps/api/src/route-controllers/student-appeal/_tests_/e2e/student-appeal.students.controller.submitStudentAppeal.e2e-spec.ts
+++ b/sources/packages/backend/apps/api/src/route-controllers/student-appeal/_tests_/e2e/student-appeal.students.controller.submitStudentAppeal.e2e-spec.ts
@@ -34,7 +34,6 @@ import { FormService } from "../../../../services";
 import {
   APPLICATION_CHANGE_NOT_ELIGIBLE,
   APPLICATION_HAS_PENDING_APPEAL,
-  INVALID_APPLICATION_NUMBER,
 } from "../../../../constants";
 
 describe("StudentAppealStudentsController(e2e)-submitStudentAppeal", () => {
@@ -254,7 +253,8 @@ describe("StudentAppealStudentsController(e2e)-submitStudentAppeal", () => {
       .expect({
         message:
           "Given application either does not exist or is not complete to request change.",
-        errorType: INVALID_APPLICATION_NUMBER,
+        error: "Not Found",
+        statusCode: HttpStatus.NOT_FOUND,
       });
   });
 
diff --git a/sources/packages/backend/apps/api/src/route-controllers/student-appeal/student-appeal.students.controller.ts b/sources/packages/backend/apps/api/src/route-controllers/student-appeal/student-appeal.students.controller.ts
index efa39e16ec..57bcf54362 100644
--- a/sources/packages/backend/apps/api/src/route-controllers/student-appeal/student-appeal.students.controller.ts
+++ b/sources/packages/backend/apps/api/src/route-controllers/student-appeal/student-appeal.students.controller.ts
@@ -43,7 +43,6 @@ import {
 import {
   APPLICATION_CHANGE_NOT_ELIGIBLE,
   APPLICATION_HAS_PENDING_APPEAL,
-  INVALID_APPLICATION_NUMBER,
 } from "../../constants";
 import { StudentAppealRequestModel } from "../../services/student-appeal/student-appeal.model";
 import { StudentAppealControllerService } from "./student-appeal.controller.service";
@@ -88,16 +87,12 @@ export class StudentAppealStudentsController extends BaseController {
   ): Promise<PrimaryIdentifierAPIOutDTO> {
     const application =
       await this.applicationService.getApplicationToRequestAppeal(
-        userToken.userId,
-        undefined,
         applicationId,
+        userToken.userId,
       );
     if (!application) {
       throw new NotFoundException(
-        new ApiProcessError(
-          "Given application either does not exist or is not complete to request change.",
-          INVALID_APPLICATION_NUMBER,
-        ),
+        "Given application either does not exist or is not complete to request change.",
       );
     }
 
diff --git a/sources/packages/backend/apps/api/src/route-controllers/student/_tests_/e2e/student.institutions.controller.getStudentApplicationSummary.e2e-spec.ts b/sources/packages/backend/apps/api/src/route-controllers/student/_tests_/e2e/student.institutions.controller.getStudentApplicationSummary.e2e-spec.ts
index 93f27ba052..89da7ea35b 100644
--- a/sources/packages/backend/apps/api/src/route-controllers/student/_tests_/e2e/student.institutions.controller.getStudentApplicationSummary.e2e-spec.ts
+++ b/sources/packages/backend/apps/api/src/route-controllers/student/_tests_/e2e/student.institutions.controller.getStudentApplicationSummary.e2e-spec.ts
@@ -85,10 +85,10 @@ describe("StudentInstitutionsController(e2e)-getStudentApplicationSummary", () =
                 savedApplication.currentAssessment.offering.studyStartDate,
               studyEndPeriod:
                 savedApplication.currentAssessment.offering.studyEndDate,
-              applicationName: "Financial Aid Application",
               status: savedApplication.applicationStatus,
               parentApplicationId: savedApplication.id,
               submittedDate: savedApplication.submittedDate.toISOString(),
+              isChangeRequestAllowedForPY: false,
             },
           ],
           count: 1,
@@ -137,10 +137,10 @@ describe("StudentInstitutionsController(e2e)-getStudentApplicationSummary", () =
                 savedApplication1.currentAssessment.offering.studyStartDate,
               studyEndPeriod:
                 savedApplication1.currentAssessment.offering.studyEndDate,
-              applicationName: "Financial Aid Application",
               status: savedApplication1.applicationStatus,
               parentApplicationId: savedApplication1.id,
               submittedDate: savedApplication1.submittedDate.toISOString(),
+              isChangeRequestAllowedForPY: false,
             },
           ],
           count: 1,
@@ -194,10 +194,10 @@ describe("StudentInstitutionsController(e2e)-getStudentApplicationSummary", () =
                 savedApplication1.currentAssessment.offering.studyStartDate,
               studyEndPeriod:
                 savedApplication1.currentAssessment.offering.studyEndDate,
-              applicationName: "Financial Aid Application",
               status: savedApplication1.applicationStatus,
               parentApplicationId: savedApplication1.id,
               submittedDate: savedApplication1.submittedDate.toISOString(),
+              isChangeRequestAllowedForPY: false,
             },
           ],
           count: 2,
@@ -255,10 +255,10 @@ describe("StudentInstitutionsController(e2e)-getStudentApplicationSummary", () =
                 savedApplication2.currentAssessment.offering.studyStartDate,
               studyEndPeriod:
                 savedApplication2.currentAssessment.offering.studyEndDate,
-              applicationName: "Financial Aid Application",
               status: savedApplication2.applicationStatus,
               parentApplicationId: savedApplication2.id,
               submittedDate: savedApplication2.submittedDate.toISOString(),
+              isChangeRequestAllowedForPY: false,
             },
             {
               id: savedApplication1.id,
@@ -267,10 +267,10 @@ describe("StudentInstitutionsController(e2e)-getStudentApplicationSummary", () =
                 savedApplication1.currentAssessment.offering.studyStartDate,
               studyEndPeriod:
                 savedApplication1.currentAssessment.offering.studyEndDate,
-              applicationName: "Financial Aid Application",
               status: savedApplication1.applicationStatus,
               parentApplicationId: savedApplication1.id,
               submittedDate: savedApplication1.submittedDate.toISOString(),
+              isChangeRequestAllowedForPY: false,
             },
           ],
           count: 2,
diff --git a/sources/packages/backend/apps/api/src/route-controllers/student/models/student.dto.ts b/sources/packages/backend/apps/api/src/route-controllers/student/models/student.dto.ts
index b9fc39131e..16e09c24eb 100644
--- a/sources/packages/backend/apps/api/src/route-controllers/student/models/student.dto.ts
+++ b/sources/packages/backend/apps/api/src/route-controllers/student/models/student.dto.ts
@@ -210,10 +210,17 @@ export class ApplicationSummaryAPIOutDTO {
   studyStartPeriod: string;
   studyEndPeriod: string;
   id: number;
-  applicationName: string;
   status: ApplicationStatus;
   parentApplicationId: number;
+  /**
+   * Original application submission date.
+   */
   submittedDate?: Date;
+  /**
+   * Indicates if the application is able to use the
+   * change request feature. Other conditions may apply.
+   */
+  isChangeRequestAllowedForPY: boolean;
 }
 
 /**
diff --git a/sources/packages/backend/apps/api/src/route-controllers/student/student.controller.service.ts b/sources/packages/backend/apps/api/src/route-controllers/student/student.controller.service.ts
index d649a63f45..31c631f07a 100644
--- a/sources/packages/backend/apps/api/src/route-controllers/student/student.controller.service.ts
+++ b/sources/packages/backend/apps/api/src/route-controllers/student/student.controller.service.ts
@@ -16,7 +16,10 @@ import {
   ApplicationPaginationOptionsAPIInDTO,
   PaginatedResultsAPIOutDTO,
 } from "../models/pagination.dto";
-import { getUserFullName } from "../../utilities";
+import {
+  allowApplicationChangeRequest,
+  getUserFullName,
+} from "../../utilities";
 import {
   CustomNamedError,
   getISODateOnlyString,
@@ -358,11 +361,12 @@ export class StudentControllerService {
       applicationNumber: application.applicationNumber,
       studyStartPeriod: getISODateOnlyString(offering?.studyStartDate),
       studyEndPeriod: getISODateOnlyString(offering?.studyEndDate),
-      // TODO: when application name is captured, update the below line
-      applicationName: "Financial Aid Application",
       status: application.applicationStatus,
       parentApplicationId: application.parentApplication.id,
       submittedDate: application.parentApplication.submittedDate,
+      isChangeRequestAllowedForPY: allowApplicationChangeRequest(
+        application.programYear,
+      ),
     };
   };
 
diff --git a/sources/packages/backend/apps/api/src/services/application/application.service.ts b/sources/packages/backend/apps/api/src/services/application/application.service.ts
index e33bfd2f72..f86119485c 100644
--- a/sources/packages/backend/apps/api/src/services/application/application.service.ts
+++ b/sources/packages/backend/apps/api/src/services/application/application.service.ts
@@ -720,8 +720,10 @@ export class ApplicationService extends RecordDataModelService<Application> {
         "offering.studyStartDate",
         "offering.studyEndDate",
         "application.applicationStatus",
+        "programYear.programYear",
       ])
       .innerJoin("application.parentApplication", "parentApplication")
+      .innerJoin("application.programYear", "programYear")
       .leftJoin("application.currentAssessment", "currentAssessment")
       .leftJoin("currentAssessment.offering", "offering")
       .where("application.student.id = :studentId", { studentId })
@@ -1427,19 +1429,14 @@ export class ApplicationService extends RecordDataModelService<Application> {
   }
 
   /**
-   * Retrieve application with application number in
-   * completed status.
-   ** Application id is used to perform same lookup by id
-   ** instead of application number.
-   * @param userId
-   * @param applicationId
-   * @param applicationNumber
-   * @returns
+   * Retrieves the application to request an appeal.
+   * @param applicationId application ID.
+   * @param userId user id.
+   * @returns application details for an appeal.
    */
   async getApplicationToRequestAppeal(
+    applicationId: number,
     userId: number,
-    applicationNumber?: string,
-    applicationId?: number,
   ): Promise<Application> {
     return this.repo.findOne({
       select: {
@@ -1452,7 +1449,6 @@ export class ApplicationService extends RecordDataModelService<Application> {
       where: {
         student: { user: { id: userId } },
         applicationStatus: ApplicationStatus.Completed,
-        applicationNumber,
         id: applicationId,
       },
     });
@@ -1869,18 +1865,22 @@ export class ApplicationService extends RecordDataModelService<Application> {
   /**
    * Get all the application versions for an application through parent application.
    * @param applicationId application id.
-   * @returns application versions.
+   * @param options query options.
+   * - `studentId` student ID used for authorization.
+   * @returns application versions if any, otherwise empty array.
    */
   async getAllApplicationVersions(
     applicationId: number,
+    options?: { studentId?: number },
   ): Promise<Application[]> {
-    const application = await this.repo
+    const applicationQuery = this.repo
       .createQueryBuilder("application")
       .select([
         "application.id",
         "parentApplication.id",
         "version.id",
         "version.submittedDate",
+        "version.applicationEditStatus",
       ])
       .innerJoin("application.parentApplication", "parentApplication")
       .leftJoin(
@@ -1892,8 +1892,19 @@ export class ApplicationService extends RecordDataModelService<Application> {
       .where("application.id = :applicationId", {
         applicationId: applicationId,
       })
-      .orderBy("version.submittedDate", "DESC")
-      .getOne();
+      .orderBy("version.submittedDate", "DESC");
+    if (options?.studentId) {
+      applicationQuery.andWhere("application.student.id = :studentId", {
+        studentId: options.studentId,
+      });
+    }
+    const application = await applicationQuery.getOne();
+    if (!application) {
+      throw new CustomNamedError(
+        "Application not found.",
+        APPLICATION_NOT_FOUND,
+      );
+    }
     return application.parentApplication.versions;
   }
 
diff --git a/sources/packages/backend/apps/api/src/utilities/program-year-utils.ts b/sources/packages/backend/apps/api/src/utilities/program-year-utils.ts
index 43a80e5a51..a7e7dea0ad 100644
--- a/sources/packages/backend/apps/api/src/utilities/program-year-utils.ts
+++ b/sources/packages/backend/apps/api/src/utilities/program-year-utils.ts
@@ -41,3 +41,20 @@ export function offeringBelongToProgramYear(
   // Check if the program start date belong to the program year.
   return isBetweenPeriod(offering.studyStartDate, programYear);
 }
+
+/**
+ * Check if a program year allows a change request.
+ * @param programYear program year entity model to be checked.
+ * @returns true if the program year allows a change request, false otherwise.
+ */
+export function allowApplicationChangeRequest(
+  programYear: Pick<ProgramYear, "programYear">,
+): boolean {
+  // Existing program years that are not allowed to submit change request.
+  // This will allow any program year from 2025-2026 and beyond to submit.
+  // Once the change request is allowed for all program years, this function
+  // should be removed.
+  return !["2021-2022", "2022-2023", "2023-2024", "2024-2025"].includes(
+    programYear.programYear,
+  );
+}
diff --git a/sources/packages/forms/src/form-definitions/studentrequestchange.json b/sources/packages/forms/src/form-definitions/studentrequestchange.json
index a1236bcce8..9cb5edf3fc 100644
--- a/sources/packages/forms/src/form-definitions/studentrequestchange.json
+++ b/sources/packages/forms/src/form-definitions/studentrequestchange.json
@@ -357,7 +357,7 @@
           "mask": false,
           "autofocus": false,
           "spellcheck": true,
-          "disabled": false,
+          "disabled": true,
           "tableView": true,
           "modalEdit": false,
           "multiple": false,
@@ -376,7 +376,7 @@
           "allowCalculateOverride": false,
           "validateOn": "change",
           "validate": {
-            "required": true,
+            "required": false,
             "minLength": "",
             "maxLength": "",
             "minWords": "",
@@ -557,148 +557,6 @@
         }
       ],
       "id": "ew7j7pq"
-    },
-    {
-      "label": "applicationId",
-      "customClass": "",
-      "modalEdit": false,
-      "persistent": true,
-      "protected": false,
-      "dbIndex": false,
-      "encrypted": false,
-      "redrawOn": "",
-      "customDefaultValue": "",
-      "calculateValue": "",
-      "calculateServer": false,
-      "key": "applicationId",
-      "tags": [],
-      "properties": {},
-      "logic": [],
-      "attributes": {},
-      "overlay": {
-        "style": "",
-        "page": "",
-        "left": "",
-        "top": "",
-        "width": "",
-        "height": ""
-      },
-      "type": "hidden",
-      "input": true,
-      "placeholder": "",
-      "prefix": "",
-      "suffix": "",
-      "multiple": false,
-      "unique": false,
-      "hidden": false,
-      "clearOnHide": true,
-      "refreshOn": "",
-      "tableView": false,
-      "labelPosition": "top",
-      "description": "",
-      "errorLabel": "",
-      "tooltip": "",
-      "hideLabel": false,
-      "tabindex": "",
-      "disabled": false,
-      "autofocus": false,
-      "widget": {
-        "type": "input"
-      },
-      "validateOn": "change",
-      "validate": {
-        "required": false,
-        "custom": "",
-        "customPrivate": false,
-        "strictDateValidation": false,
-        "multiple": false,
-        "unique": false
-      },
-      "conditional": {
-        "show": null,
-        "when": null,
-        "eq": ""
-      },
-      "allowCalculateOverride": false,
-      "showCharCount": false,
-      "showWordCount": false,
-      "allowMultipleMasks": false,
-      "inputType": "hidden",
-      "id": "em1y8gd",
-      "defaultValue": "",
-      "dataGridLabel": false,
-      "addons": []
-    },
-    {
-      "label": "applicationStatus",
-      "customClass": "",
-      "modalEdit": false,
-      "defaultValue": null,
-      "persistent": true,
-      "protected": false,
-      "dbIndex": false,
-      "encrypted": false,
-      "redrawOn": "",
-      "customDefaultValue": "",
-      "calculateValue": "",
-      "calculateServer": false,
-      "key": "applicationStatus",
-      "tags": [],
-      "properties": {},
-      "logic": [],
-      "attributes": {},
-      "overlay": {
-        "style": "",
-        "page": "",
-        "left": "",
-        "top": "",
-        "width": "",
-        "height": ""
-      },
-      "type": "hidden",
-      "input": true,
-      "tableView": false,
-      "placeholder": "",
-      "prefix": "",
-      "suffix": "",
-      "multiple": false,
-      "unique": false,
-      "hidden": false,
-      "clearOnHide": true,
-      "refreshOn": "",
-      "dataGridLabel": false,
-      "labelPosition": "top",
-      "description": "",
-      "errorLabel": "",
-      "tooltip": "",
-      "hideLabel": false,
-      "tabindex": "",
-      "disabled": false,
-      "autofocus": false,
-      "widget": {
-        "type": "input"
-      },
-      "validateOn": "change",
-      "validate": {
-        "required": false,
-        "custom": "",
-        "customPrivate": false,
-        "strictDateValidation": false,
-        "multiple": false,
-        "unique": false
-      },
-      "conditional": {
-        "show": null,
-        "when": null,
-        "eq": ""
-      },
-      "allowCalculateOverride": false,
-      "showCharCount": false,
-      "showWordCount": false,
-      "allowMultipleMasks": false,
-      "inputType": "hidden",
-      "id": "e6z1qd9",
-      "addons": []
     }
   ]
 }
\ No newline at end of file
diff --git a/sources/packages/web/src/components/students/StudentApplicationsExtendedSummary.vue b/sources/packages/web/src/components/students/StudentApplicationsExtendedSummary.vue
index ef5808e101..7619ea982f 100644
--- a/sources/packages/web/src/components/students/StudentApplicationsExtendedSummary.vue
+++ b/sources/packages/web/src/components/students/StudentApplicationsExtendedSummary.vue
@@ -21,21 +21,15 @@
           :loading="loading"
           :mobile="isMobile"
           @update:options="paginationAndSortEvent"
+          show-expand
+          v-model:expanded="expandedItems"
         >
+          <template v-slot:loading>
+            <v-skeleton-loader type="table-row@5"></v-skeleton-loader>
+          </template>
           <template #[`item.applicationNumber`]="{ item }">
             {{ item.applicationNumber }}
           </template>
-          <template #[`item.applicationName`]="{ item }">
-            <v-btn
-              variant="plain"
-              @click="$emit('goToApplication', item.id)"
-              color="primary"
-              >{{ item.applicationName }}
-              <v-tooltip activator="parent" location="start"
-                >Click to view this application</v-tooltip
-              >
-            </v-btn>
-          </template>
           <template #[`item.submitted`]="{ item }">
             {{
               emptyStringFiller(getISODateHourMinuteString(item.submittedDate))
@@ -49,38 +43,93 @@
             <status-chip-application :status="item.status" />
           </template>
           <template #[`item.actions`]="{ item }">
-            <span
-              v-if="
-                !(
-                  item.status === ApplicationStatus.Cancelled ||
-                  item.status === ApplicationStatus.Completed
-                )
-              "
-            >
+            <v-btn-group variant="text" :rounded="false">
+              <v-btn color="primary" @click="$emit('goToApplication', item.id)"
+                >View
+                <v-tooltip activator="parent" location="start"
+                  >Click to view this application</v-tooltip
+                >
+              </v-btn>
               <v-btn
+                v-if="canDisplayEditOrCancel(item)"
                 :disabled="!hasSINValidStatus"
-                variant="plain"
                 color="primary"
-                class="label-bold"
                 @click="$emit('editApplicationAction', item.status, item.id)"
                 append-icon="mdi-pencil-outline"
-                ><span class="label-bold">Edit</span>
+                >Edit
                 <v-tooltip activator="parent" location="start"
                   >Click to edit this application</v-tooltip
                 >
               </v-btn>
               <v-btn
+                v-if="canDisplayChangeRequest(item)"
+                :disabled="!hasSINValidStatus"
+                color="primary"
+                @click="
+                  $emit(
+                    'changeApplicationAction',
+                    item.id,
+                    item.isChangeRequestAllowedForPY,
+                  )
+                "
+                append-icon="mdi-pencil-outline"
+                >Change Request
+                <v-tooltip activator="parent" location="start"
+                  >Click to request a change in this application</v-tooltip
+                >
+              </v-btn>
+              <v-btn
+                v-if="canDisplayEditOrCancel(item)"
                 :disabled="!hasSINValidStatus"
-                variant="plain"
                 color="primary"
-                class="label-bold"
                 @click="emitCancel(item.id)"
-                ><span class="label-bold">Cancel</span>
+                >Cancel
                 <v-tooltip activator="parent" location="start"
                   >Click to cancel this application</v-tooltip
                 >
               </v-btn>
-            </span>
+            </v-btn-group>
+          </template>
+          <template
+            #[`item.data-table-expand`]="{
+              internalItem,
+              isExpanded,
+              toggleExpand,
+            }"
+          >
+            <v-btn
+              v-if="
+                internalItem.raw.id !== internalItem.raw.parentApplicationId
+              "
+              :append-icon="
+                isExpanded(internalItem)
+                  ? '$expanderCollapseIcon'
+                  : '$expanderExpandIcon'
+              "
+              text="Versions"
+              variant="text"
+              color="primary"
+              @click="
+                versionsExpanderClick(internalItem.raw, () =>
+                  toggleExpand(internalItem),
+                )
+              "
+            ></v-btn>
+          </template>
+          <template v-slot:expanded-row="{ columns, item }">
+            <tr>
+              <td :colspan="columns.length" class="py-4">
+                <content-group>
+                  <student-applications-version
+                    :versions="item.versions"
+                    :loading="item.loadingVersions"
+                    @viewApplicationVersion="
+                      $emit('viewApplicationVersion', $event)
+                    "
+                  />
+                </content-group>
+              </td>
+            </tr>
           </template>
         </v-data-table-server>
       </toggle-content>
@@ -104,22 +153,42 @@ import {
 import { ApplicationService } from "@/services/ApplicationService";
 import { useFormatters } from "@/composables";
 import StatusChipApplication from "@/components/generic/StatusChipApplication.vue";
+import StudentApplicationsVersion from "@/components/students/StudentApplicationsVersion.vue";
 import { useStore } from "vuex";
 import {
   ApplicationSummaryAPIOutDTO,
   PaginatedResultsAPIOutDTO,
+  ApplicationVersionAPIOutDTO,
 } from "@/services/http/dto";
 import { useDisplay } from "vuetify";
 
+interface ApplicationSummaryModel extends ApplicationSummaryAPIOutDTO {
+  versions?: ApplicationVersionAPIOutDTO[];
+  loadingVersions?: boolean;
+}
+
 export default defineComponent({
-  components: { StatusChipApplication },
-  emits: ["editApplicationAction", "openConfirmCancel", "goToApplication"],
+  components: { StatusChipApplication, StudentApplicationsVersion },
+  emits: {
+    editApplicationAction: (status: ApplicationStatus, applicationId: number) =>
+      !!status && !!applicationId,
+    changeApplicationAction: (
+      applicationId: number,
+      allowChangeRequest: boolean,
+    ) => !!applicationId && !!allowChangeRequest,
+    openConfirmCancel: (applicationId: number, callback: () => void) =>
+      !!applicationId && !!callback,
+    goToApplication: (applicationId: number) => !!applicationId,
+    viewApplicationVersion: (applicationId: number) => !!applicationId,
+  },
   setup(_, { emit }) {
     const loading = ref(false);
     const { mobile: isMobile } = useDisplay();
+    const expandedItems = ref([]);
     const applicationsAndCount = ref(
-      {} as PaginatedResultsAPIOutDTO<ApplicationSummaryAPIOutDTO>,
+      {} as PaginatedResultsAPIOutDTO<ApplicationSummaryModel>,
     );
+
     const {
       dateOnlyLongString,
       getISODateHourMinuteString,
@@ -143,6 +212,8 @@ export default defineComponent({
     const getStudentApplications = async () => {
       try {
         loading.value = true;
+        // Reset expanded items when fetching new data.
+        expandedItems.value = [];
         applicationsAndCount.value =
           await ApplicationService.shared.getStudentApplicationSummary(
             currentPagination.value.page - 1,
@@ -155,11 +226,7 @@ export default defineComponent({
       }
     };
 
-    const reloadApplications = async () => {
-      await getStudentApplications();
-    };
-
-    onMounted(reloadApplications);
+    onMounted(getStudentApplications);
 
     const paginationAndSortEvent = async (event: DataTableOptions) => {
       currentPagination.value.page = event.page;
@@ -181,13 +248,54 @@ export default defineComponent({
     };
 
     const emitCancel = (applicationId: number) => {
-      emit("openConfirmCancel", applicationId, () => reloadApplications());
+      emit("openConfirmCancel", applicationId, () => getStudentApplications());
+    };
+
+    const versionsExpanderClick = async (
+      application: ApplicationSummaryModel,
+      toggleExpand: () => void,
+    ) => {
+      toggleExpand();
+      if (application.versions) {
+        // Application versions are not required to be fetched every time.
+        // If they are already loaded there is no critical reason to fetch them again.
+        return;
+      }
+      try {
+        application.loadingVersions = true;
+        const applications =
+          await ApplicationService.shared.getApplicationOverallDetails(
+            application.id,
+          );
+        application.versions = applications.previousVersions;
+      } catch (error) {
+        console.error(error);
+      } finally {
+        application.loadingVersions = false;
+      }
+    };
+
+    const canDisplayEditOrCancel = (
+      application: ApplicationSummaryAPIOutDTO,
+    ) => {
+      return (
+        application.status !== ApplicationStatus.Cancelled &&
+        application.status !== ApplicationStatus.Completed
+      );
+    };
+
+    const canDisplayChangeRequest = (
+      application: ApplicationSummaryAPIOutDTO,
+    ) => {
+      return application.status === ApplicationStatus.Completed;
     };
 
     return {
       dateOnlyLongString,
       getISODateHourMinuteString,
       emptyStringFiller,
+      canDisplayEditOrCancel,
+      canDisplayChangeRequest,
       ApplicationStatus,
       applicationsAndCount,
       DEFAULT_PAGE_LIMIT,
@@ -199,6 +307,8 @@ export default defineComponent({
       StudentApplicationsExtendedSummaryHeaders,
       isMobile,
       paginationAndSortEvent,
+      versionsExpanderClick,
+      expandedItems,
     };
   },
 });
diff --git a/sources/packages/web/src/components/students/StudentApplicationsVersion.vue b/sources/packages/web/src/components/students/StudentApplicationsVersion.vue
new file mode 100644
index 0000000000..92bd34b56d
--- /dev/null
+++ b/sources/packages/web/src/components/students/StudentApplicationsVersion.vue
@@ -0,0 +1,69 @@
+<template>
+  <v-table>
+    <thead>
+      <tr>
+        <th id="submitted-header">Submitted</th>
+        <th id="status-header">Status</th>
+        <th id="actions-header">Actions</th>
+      </tr>
+    </thead>
+    <tbody>
+      <tr v-if="loading">
+        <td colspan="3">
+          <v-skeleton-loader type="table-row@2" />
+        </td>
+      </tr>
+      <tr v-for="version in versions" :key="version.id">
+        <td headers="submitted-header">
+          {{ getISODateHourMinuteString(version.submittedDate) }}
+        </td>
+        <td headers="status-header">
+          {{
+            mapApplicationEditStatusForStudents(version.applicationEditStatus)
+          }}
+        </td>
+        <td headers="actions-header">
+          <v-btn
+            variant="text"
+            color="primary"
+            @click="$emit('viewApplicationVersion', version.id)"
+            >View
+            <v-tooltip activator="parent" location="start"
+              >Click to view this application version</v-tooltip
+            >
+          </v-btn>
+        </td>
+      </tr>
+    </tbody>
+  </v-table>
+</template>
+
+<script lang="ts">
+import { defineComponent } from "vue";
+import { useApplication, useFormatters } from "@/composables";
+import { ApplicationVersionAPIOutDTO } from "@/services/http/dto";
+
+export default defineComponent({
+  emits: {
+    viewApplicationVersion: (applicationId: number) => !!applicationId,
+  },
+  props: {
+    versions: {
+      type: Array<ApplicationVersionAPIOutDTO>,
+      required: false,
+    },
+    loading: {
+      type: Boolean,
+      required: false,
+    },
+  },
+  setup() {
+    const { mapApplicationEditStatusForStudents } = useApplication();
+    const { getISODateHourMinuteString } = useFormatters();
+    return {
+      getISODateHourMinuteString,
+      mapApplicationEditStatusForStudents,
+    };
+  },
+});
+</script>
diff --git a/sources/packages/web/src/composables/useApplication.ts b/sources/packages/web/src/composables/useApplication.ts
index 05a0536ae7..31197de566 100644
--- a/sources/packages/web/src/composables/useApplication.ts
+++ b/sources/packages/web/src/composables/useApplication.ts
@@ -1,5 +1,6 @@
 import {
   ApplicationDetailHeader,
+  ApplicationEditStatus,
   ApplicationStatus,
   OfferingIntensity,
   StatusChipTypes,
@@ -55,5 +56,30 @@ export function useApplication() {
         "-",
     };
   };
-  return { mapApplicationChipStatus, mapApplicationDetailHeader };
+
+  /**
+   * Application edit status targeting students.
+   * @param editStatus application edit status.
+   * @returns student friendly edit status.
+   */
+  const mapApplicationEditStatusForStudents = (
+    editStatus: ApplicationEditStatus,
+  ): string => {
+    switch (editStatus) {
+      case ApplicationEditStatus.ChangeDeclined:
+        return "Declined";
+      case ApplicationEditStatus.ChangeCancelled:
+        return "Cancelled";
+      case ApplicationEditStatus.ChangedWithApproval:
+        return "Changed";
+      default:
+        return editStatus;
+    }
+  };
+
+  return {
+    mapApplicationChipStatus,
+    mapApplicationDetailHeader,
+    mapApplicationEditStatusForStudents,
+  };
 }
diff --git a/sources/packages/web/src/constants/error-code.constants.ts b/sources/packages/web/src/constants/error-code.constants.ts
index c331d50756..8266e85dd0 100644
--- a/sources/packages/web/src/constants/error-code.constants.ts
+++ b/sources/packages/web/src/constants/error-code.constants.ts
@@ -10,7 +10,6 @@ export const APPLICATION_CHANGE_NOT_ELIGIBLE =
  * An application can have only one pending appeal at a time.
  */
 export const APPLICATION_HAS_PENDING_APPEAL = "APPLICATION_HAS_PENDING_APPEAL";
-export const INVALID_APPLICATION_NUMBER = "INVALID_APPLICATION_NUMBER";
 export const FIRST_COE_NOT_COMPLETE = "FIRST_COE_NOT_COMPLETE";
 export const INVALID_TUITION_REMITTANCE_AMOUNT =
   "INVALID_TUITION_REMITTANCE_AMOUNT";
diff --git a/sources/packages/web/src/router/StudentRoutes.ts b/sources/packages/web/src/router/StudentRoutes.ts
index abde39220c..cd322bd3ea 100644
--- a/sources/packages/web/src/router/StudentRoutes.ts
+++ b/sources/packages/web/src/router/StudentRoutes.ts
@@ -175,6 +175,9 @@ export const studentRoutes: Array<RouteRecordRaw> = [
         path: AppRoutes.RequestChange,
         name: StudentRoutesConst.STUDENT_REQUEST_CHANGE,
         component: StudentAppealRequest,
+        props: (route) => ({
+          applicationId: parseInt(route.params.applicationId as string),
+        }),
         meta: {
           clientType: ClientIdType.Student,
         },
diff --git a/sources/packages/web/src/services/ApplicationService.ts b/sources/packages/web/src/services/ApplicationService.ts
index d058ac1268..22b71011b3 100644
--- a/sources/packages/web/src/services/ApplicationService.ts
+++ b/sources/packages/web/src/services/ApplicationService.ts
@@ -153,11 +153,9 @@ export class ApplicationService {
   }
 
   async getApplicationForRequestChange(
-    applicationNumber: string,
+    applicationId: number,
   ): Promise<ApplicationProgramYearAPIOutDTO> {
-    return ApiClient.Application.getApplicationForRequestChange(
-      applicationNumber,
-    );
+    return ApiClient.Application.getApplicationForRequestChange(applicationId);
   }
 
   /**
diff --git a/sources/packages/web/src/services/http/ApplicationApi.ts b/sources/packages/web/src/services/http/ApplicationApi.ts
index 872a726867..b4f2c53176 100644
--- a/sources/packages/web/src/services/http/ApplicationApi.ts
+++ b/sources/packages/web/src/services/http/ApplicationApi.ts
@@ -163,10 +163,10 @@ export class ApplicationApi extends HttpBaseClient {
   }
 
   async getApplicationForRequestChange(
-    applicationNumber: string,
+    applicationId: number,
   ): Promise<ApplicationProgramYearAPIOutDTO> {
     return this.getCall<ApplicationProgramYearAPIOutDTO>(
-      this.addClientRoot(`application/${applicationNumber}/appeal`),
+      this.addClientRoot(`application/${applicationId}/appeal`),
     );
   }
 
diff --git a/sources/packages/web/src/services/http/dto/Application.dto.ts b/sources/packages/web/src/services/http/dto/Application.dto.ts
index f47eb8c55e..78e71cb027 100644
--- a/sources/packages/web/src/services/http/dto/Application.dto.ts
+++ b/sources/packages/web/src/services/http/dto/Application.dto.ts
@@ -14,6 +14,7 @@ import {
   StudentAssessmentStatus,
   ECertFailedValidation,
   ChangeTypes,
+  ApplicationEditStatus,
 } from "@/types";
 
 export interface InProgressApplicationDetailsAPIOutDTO {
@@ -175,6 +176,7 @@ export interface ApplicationWarningsAPIOutDTO {
 export interface ApplicationVersionAPIOutDTO {
   id: number;
   submittedDate: Date;
+  applicationEditStatus: ApplicationEditStatus;
 }
 
 export interface ApplicationOverallDetailsAPIOutDTO {
diff --git a/sources/packages/web/src/services/http/dto/Student.dto.ts b/sources/packages/web/src/services/http/dto/Student.dto.ts
index 63eabbf332..ba5e2dea75 100644
--- a/sources/packages/web/src/services/http/dto/Student.dto.ts
+++ b/sources/packages/web/src/services/http/dto/Student.dto.ts
@@ -137,15 +137,25 @@ export interface SearchStudentAPIOutDTO {
   sin: string;
 }
 
+/**
+ * DTO object application summary info.
+ */
 export interface ApplicationSummaryAPIOutDTO {
   applicationNumber: string;
   studyStartPeriod: string;
   studyEndPeriod: string;
   id: number;
-  applicationName: string;
   status: ApplicationStatus;
   parentApplicationId: number;
+  /**
+   * Original application submission date.
+   */
   submittedDate?: Date;
+  /**
+   * Indicates if the application is able to use the
+   * change request feature. Other conditions may apply.
+   */
+  isChangeRequestAllowedForPY: boolean;
 }
 
 /**
diff --git a/sources/packages/web/src/types/AppRoutes.ts b/sources/packages/web/src/types/AppRoutes.ts
index 75685c4a7a..cdb9d60022 100644
--- a/sources/packages/web/src/types/AppRoutes.ts
+++ b/sources/packages/web/src/types/AppRoutes.ts
@@ -9,7 +9,7 @@ export enum AppRoutes {
   StudentFileUploader = "file-uploader",
   StudentApplicationDetails = "application/:id",
   Assessment = "application/:applicationId/assessment/:assessmentId",
-  RequestChange = "request-change",
+  RequestChange = "application/:applicationId/request-change",
   StudentAccountActivity = "student-account-activity",
   StudentOverawardsBalance = "student-overawards-balance",
   StudentAccountApplicationIsProgress = "student-account-application-in-progress",
diff --git a/sources/packages/web/src/types/contracts/DataTableContract.ts b/sources/packages/web/src/types/contracts/DataTableContract.ts
index 6a8edcb5f6..f531bba2bb 100644
--- a/sources/packages/web/src/types/contracts/DataTableContract.ts
+++ b/sources/packages/web/src/types/contracts/DataTableContract.ts
@@ -23,7 +23,6 @@ export enum UserFields {
  */
 export enum StudentApplicationFields {
   ApplicationNumber = "applicationNumber",
-  ApplicationName = "applicationName",
   StudyPeriod = "studyPeriod",
   Submitted = "submitted",
   Status = "status",
@@ -486,9 +485,9 @@ export const StudentApplicationsSimplifiedSummaryHeaders = [
  */
 export const StudentApplicationsExtendedSummaryHeaders = [
   { title: "Application Number", sortable: true, key: "applicationNumber" },
-  { title: "Name", sortable: false, key: "applicationName" },
   { title: "Submitted", sortable: false, key: "submitted" },
   { title: "Study Period", sortable: false, key: "studyStartPeriod" },
   { title: "Status", sortable: true, key: "status" },
   { title: "Actions", sortable: false, key: "actions" },
+  { title: "History", sortable: false, key: "data-table-expand" },
 ];
diff --git a/sources/packages/web/src/types/contracts/student/ApplicationContract.ts b/sources/packages/web/src/types/contracts/student/ApplicationContract.ts
index 9cac400bd1..1f3b774fe8 100644
--- a/sources/packages/web/src/types/contracts/student/ApplicationContract.ts
+++ b/sources/packages/web/src/types/contracts/student/ApplicationContract.ts
@@ -56,6 +56,53 @@ export enum ApplicationStatus {
   Edited = "Edited",
 }
 
+/**
+ * Status of the "application edit"(a.k.a. change request) that allows to track the changes and approvals.
+ * An application edition can be requested before or after an application is completed, which leads
+ * to different paths controlled by this status.
+ * If the application is edited before is has its main status set to "Completed" (a.k.a. before COE),
+ * the edit can be executed without any Ministry approval.
+ * If the application is edited after it has its main status set to "Completed" (a.k.a. after COE),
+ * the edit must be approved by the Ministry.
+ */
+export enum ApplicationEditStatus {
+  /**
+   * Default status when the application was never edited in any way.
+   * This status will likely be associated to draft applications.
+   */
+  Original = "Original",
+  /**
+   * Status for applications that are being edited after COE and Ministry approval is needed.
+   */
+  ChangeInProgress = "Change in progress",
+  /**
+   * Status for applications that are being edited after COE and Ministry approval
+   * was requested and is pending.
+   * This status is only applicable to applications that are being edited after COE.
+   */
+  ChangePendingApproval = "Change pending approval",
+  /**
+   * Status for applications that are being edited after COE and Ministry declined the edit.
+   * This status is only applicable to applications that are being edited after COE.
+   */
+  ChangeDeclined = "Change declined",
+  /**
+   * Status for applications the student opted by cancelling a change request prior to the Ministry approval.
+   * This status is only applicable to applications that are being edited after COE.
+   */
+  ChangeCancelled = "Change cancelled",
+  /**
+   * Status for applications that are being edited after COE and Ministry has approved it.
+   * This status is only applicable to applications that are being edited after COE.
+   */
+  ChangedWithApproval = "Changed with approval",
+  /**
+   * When the edit was executed and no Ministry approval was needed (a.k.a. edit before COE).
+   * This status is only applicable to applications that are being edited before COE.
+   */
+  Edited = "Edited",
+}
+
 /**
  * Possible status of application scholastic standing.
  */
diff --git a/sources/packages/web/src/views/student/AppStudent.vue b/sources/packages/web/src/views/student/AppStudent.vue
index e483510181..b4ab2ac42e 100644
--- a/sources/packages/web/src/views/student/AppStudent.vue
+++ b/sources/packages/web/src/views/student/AppStudent.vue
@@ -42,14 +42,6 @@
           prepend-icon="fa:far fa-file-alt"
           >File Uploader</v-btn
         >
-        <v-btn
-          v-if="hasAuthenticatedStudentAccount"
-          class="nav-item-label"
-          variant="text"
-          :to="{ name: StudentRoutesConst.STUDENT_REQUEST_CHANGE }"
-          prepend-icon="fa:far fa-hand-paper"
-          >Request a Change</v-btn
-        >
         <v-menu v-if="isAuthenticated">
           <template v-slot:activator="{ props }">
             <v-btn
@@ -207,14 +199,6 @@ export default defineComponent({
                 },
               },
             },
-            {
-              title: "Request a Change",
-              props: {
-                to: {
-                  name: StudentRoutesConst.STUDENT_REQUEST_CHANGE,
-                },
-              },
-            },
           );
         }
         items.push(
diff --git a/sources/packages/web/src/views/student/StudentAppealRequest.vue b/sources/packages/web/src/views/student/StudentAppealRequest.vue
index cb0a1cb034..071f0587b6 100644
--- a/sources/packages/web/src/views/student/StudentAppealRequest.vue
+++ b/sources/packages/web/src/views/student/StudentAppealRequest.vue
@@ -3,11 +3,11 @@
     <template #header>
       <header-navigator title="Student" subTitle="Request a Change" />
     </template>
-
     <div v-if="showRequestForAppeal">
       <!-- Content area. -->
       <formio-container
         formName="studentRequestChange"
+        :formData="initialData"
         @submitted="submitRequest"
       >
         <template #actions="{ submit }">
@@ -62,7 +62,7 @@
   </student-page-container>
 </template>
 <script lang="ts">
-import { computed, ref, defineComponent } from "vue";
+import { computed, ref, defineComponent, onMounted } from "vue";
 import { ApiProcessError, FormIOForm, StudentAppealRequest } from "@/types";
 import { ApplicationService } from "@/services/ApplicationService";
 import { StudentAppealService } from "@/services/StudentAppealService";
@@ -71,10 +71,12 @@ import { useSnackBar } from "@/composables";
 import {
   APPLICATION_CHANGE_NOT_ELIGIBLE,
   APPLICATION_HAS_PENDING_APPEAL,
-  INVALID_APPLICATION_NUMBER,
 } from "@/constants";
+import { ApplicationProgramYearAPIOutDTO } from "@/services/http/dto";
 
-// Model for student request change form.
+/**
+ * Model for student request change form.
+ */
 interface StudentRequestSelectedForms {
   applicationNumber: string;
   formNames: string[];
@@ -84,43 +86,51 @@ export default defineComponent({
   components: {
     AppealRequestsForm,
   },
-  setup() {
+  props: {
+    applicationId: {
+      type: Number,
+      required: true,
+    },
+  },
+  setup(props) {
     const snackBar = useSnackBar();
     const processing = ref(false);
     const appealRequestsForms = ref([] as StudentAppealRequest[]);
-    let applicationId: number;
+    const initialData = ref({} as StudentRequestSelectedForms);
+    let applicationAppealData: ApplicationProgramYearAPIOutDTO;
     const showRequestForAppeal = computed(
       () => appealRequestsForms.value.length === 0,
     );
 
-    const submitRequest = async (
-      form: FormIOForm<StudentRequestSelectedForms>,
-    ) => {
+    onMounted(async () => {
       try {
-        const application =
+        applicationAppealData =
           await ApplicationService.shared.getApplicationForRequestChange(
-            form.data.applicationNumber,
+            props.applicationId,
           );
-        applicationId = application.id;
-        appealRequestsForms.value = form.data.formNames.map(
-          (formName) =>
-            ({
-              formName,
-              data: { programYear: application.programYear },
-            } as StudentAppealRequest),
-        );
+        initialData.value = {
+          applicationNumber: applicationAppealData.applicationNumber,
+          formNames: [],
+        };
       } catch (error: unknown) {
-        const errorMessage = "An error happened while requesting a change.";
-        const errorLabel = "Unexpected error";
-        if (error instanceof ApiProcessError) {
-          if (error.errorType === INVALID_APPLICATION_NUMBER) {
-            snackBar.warn(`Application not found. ${error.message}`);
-            return;
-          }
-          snackBar.error(`${errorLabel}. ${errorMessage}`);
-        }
+        snackBar.error(
+          "An unexpected error happened while retrieving the application to request a change.",
+        );
       }
+    });
+
+    const submitRequest = async (
+      form: FormIOForm<StudentRequestSelectedForms>,
+    ) => {
+      appealRequestsForms.value = form.data.formNames.map(
+        (formName) =>
+          ({
+            formName,
+            data: { programYear: applicationAppealData.programYear },
+          } as StudentAppealRequest),
+      );
     };
+
     const backToRequestForm = () => {
       appealRequestsForms.value = [];
     };
@@ -129,18 +139,16 @@ export default defineComponent({
       try {
         processing.value = true;
         await StudentAppealService.shared.submitStudentAppeal(
-          applicationId,
+          props.applicationId,
           appealRequests,
         );
         snackBar.success(
           "The request for change has been submitted successfully.",
         );
-        //TODO: Redirect to appeal view page once it is developed.
         backToRequestForm();
       } catch (error: unknown) {
         if (error instanceof ApiProcessError) {
           switch (error.errorType) {
-            case INVALID_APPLICATION_NUMBER:
             case APPLICATION_CHANGE_NOT_ELIGIBLE:
               snackBar.warn(`Not able to submit. ${error.message}`);
               break;
@@ -155,6 +163,7 @@ export default defineComponent({
     };
 
     return {
+      initialData,
       submitRequest,
       appealRequestsForms,
       showRequestForAppeal,
diff --git a/sources/packages/web/src/views/student/StudentApplicationDetails.vue b/sources/packages/web/src/views/student/StudentApplicationDetails.vue
index 62c98b0d0b..6789cef13f 100644
--- a/sources/packages/web/src/views/student/StudentApplicationDetails.vue
+++ b/sources/packages/web/src/views/student/StudentApplicationDetails.vue
@@ -213,6 +213,9 @@ export default defineComponent({
           command: () => {
             router.push({
               name: StudentRoutesConst.STUDENT_REQUEST_CHANGE,
+              params: {
+                applicationId: props.id,
+              },
             });
           },
         });
diff --git a/sources/packages/web/src/views/student/StudentApplicationSummary.vue b/sources/packages/web/src/views/student/StudentApplicationSummary.vue
index ab645b8f82..8966a4101e 100644
--- a/sources/packages/web/src/views/student/StudentApplicationSummary.vue
+++ b/sources/packages/web/src/views/student/StudentApplicationSummary.vue
@@ -12,8 +12,10 @@
         <student-applications-extended-summary
           :dense="isMobile"
           @editApplicationAction="editApplicationAction"
+          @changeApplicationAction="changeApplicationAction"
           @openConfirmCancel="confirmCancelApplication"
           @goToApplication="goToApplication"
+          @viewApplicationVersion="goToApplicationForm"
         />
       </v-col>
     </v-row>
@@ -58,7 +60,7 @@ export default defineComponent({
       return ApplicationService.shared.getApplicationWithPY(applicationId);
     };
 
-    const goToEditApplication = async (applicationId: number) => {
+    const goToApplicationForm = async (applicationId: number) => {
       try {
         const applicationWithPY = await getApplicationWithPY(applicationId);
         router.push({
@@ -77,9 +79,27 @@ export default defineComponent({
       }
     };
 
+    const changeApplicationAction = async (
+      applicationId: number,
+      allowChangeRequest: boolean,
+    ) => {
+      if (allowChangeRequest) {
+        snackBar.warn(
+          "Change requests for the 2025-2026 program year are not available at this moment.",
+        );
+      } else {
+        router.push({
+          name: StudentRoutesConst.STUDENT_REQUEST_CHANGE,
+          params: {
+            applicationId,
+          },
+        });
+      }
+    };
+
     const confirmEditApplication = async (applicationId: number) => {
       if (await editApplicationModal.value.showModal()) {
-        await goToEditApplication(applicationId);
+        await goToApplicationForm(applicationId);
       }
     };
 
@@ -90,7 +110,7 @@ export default defineComponent({
       if (status !== ApplicationStatus.Draft) {
         await confirmEditApplication(applicationId);
       } else {
-        await goToEditApplication(applicationId);
+        await goToApplicationForm(applicationId);
       }
     };
 
@@ -116,12 +136,14 @@ export default defineComponent({
     return {
       ApplicationStatus,
       editApplicationAction,
+      changeApplicationAction,
       editApplicationModal,
       confirmCancelApplication,
       showModal,
       showHideCancelApplication,
       reloadData,
       goToApplication,
+      goToApplicationForm,
       cancelApplicationModal,
       isMobile,
     };


Reviews:
user: login: dheepak-aot
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-25T21:20:37Z
user: login: dheepak-aot
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-25T21:22:30Z
user: login: andrewsignori-aot
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-25T21:26:49Z
user: login: andrewsignori-aot
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-25T21:26:53Z
user: login: dheepak-aot
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-25T21:28:08Z
user: login: andrewsignori-aot
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-25T21:30:12Z
user: login: dheepak-aot
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-25T21:32:44Z
user: login: dheepak-aot
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-25T21:34:00Z
user: login: dheepak-aot
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-25T21:45:22Z
user: login: dheepak-aot
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-25T21:49:14Z
user: login: dheepak-aot
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-25T21:55:45Z
user: login: andrewsignori-aot
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-25T21:57:09Z
user: login: andrewsignori-aot
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-25T21:58:55Z
user: login: andrewsignori-aot
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-25T22:02:15Z
user: login: dheepak-aot
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-25T22:03:05Z
user: login: andrewsignori-aot
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-25T22:04:24Z
user: login: andrewsignori-aot
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-25T22:05:37Z
user: login: andrewsignori-aot
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-25T22:07:07Z
user: login: dheepak-aot
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-25T22:23:18Z
user: login: andrewsignori-aot
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-25T22:38:13Z
user: login: dheepak-aot
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-25T22:40:13Z
user: login: andrewsignori-aot
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-25T22:42:46Z
user: login: dheepak-aot
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-25T22:57:13Z
user: login: dheepak-aot
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-25T23:23:08Z
user: login: dheepak-aot
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-25T23:33:54Z
user: login: andrewsignori-aot
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-25T23:43:49Z
user: login: andrewsignori-aot
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-25T23:52:04Z
user: login: andrewsignori-aot
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-25T23:59:02Z
user: login: andrewsignori-aot
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-25T23:59:58Z
user: login: sh16011993
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-26T00:06:09Z
user: login: andrewsignori-aot
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-26T15:34:11Z
user: login: dheepak-aot
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-26T16:16:11Z
user: login: dheepak-aot
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-26T16:21:42Z
user: login: dheepak-aot
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-26T16:23:54Z
user: login: sh16011993
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-26T16:27:46Z
user: login: dheepak-aot
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-26T16:30:33Z
user: login: sh16011993
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-26T18:09:07Z
user: login: dheepak-aot
body: Thanks for making the changes and conversations. Looks good 👍
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-03-26T18:15:16Z
user: login: andrewsignori-aot
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-26T18:49:08Z
user: login: andrewsignori-aot
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-26T18:50:06Z
user: login: andrewsignori-aot
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-26T18:53:59Z
user: login: sh16011993
body: Great work @andrewsignori-aot 👍 
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-03-26T19:40:15Z

Review comments:
diff_hunk: @@ -547,26 +551,23 @@ export class ApplicationStudentsController extends BaseController {
   }
 
   /**
-   * Get application to request appeal.
-   ** Application eligible to be requested for
-   ** a change will be returned.
-   * @param applicationNumber
-   * @param userToken
-   * @returns application
+   * Get application to request an appeal.
+   * @param applicationId application ID.
+   * @returns application eligible to be requested for a change.
    */
   @ApiNotFoundResponse({
     description:
       "Application either not found or not eligible to request for change.",
   })
-  @Get(":applicationNumber/appeal")
+  @Get(":applicationId/appeal")
   async getApplicationToRequestAppeal(
-    @Param() applicationNumberParam: ApplicationNumberParamAPIInDTO,
+    @Param("applicationId") applicationId: number,
path: sources/packages/backend/apps/api/src/route-controllers/application/application.students.controller.ts
user: login: dheepak-aot
body: Please use the `ParseIntPipe`.
created_at: 2025-03-25T21:20:37Z
author_association: COLLABORATOR
diff_hunk: @@ -34,12 +34,12 @@ import {
   ApplicationDataAPIOutDTO,
   ApplicationWithProgramYearAPIOutDTO,
   ApplicationProgramYearAPIOutDTO,
-  ApplicationNumberParamAPIInDTO,
   InProgressApplicationDetailsAPIOutDTO,
   ApplicationProgressDetailsAPIOutDTO,
   EnrolmentApplicationDetailsAPIOutDTO,
   CompletedApplicationDetailsAPIOutDTO,
   ApplicationWarningsAPIOutDTO,
+  ApplicationOverallDetailsAPIOutDTO,
path: sources/packages/backend/apps/api/src/route-controllers/application/application.students.controller.ts
user: login: dheepak-aot
body: Please remove the un-used `ApplicationNumberParamAPIInDTO` class as well.
created_at: 2025-03-25T21:22:30Z
author_association: COLLABORATOR
diff_hunk: @@ -34,12 +34,12 @@ import {
   ApplicationDataAPIOutDTO,
   ApplicationWithProgramYearAPIOutDTO,
   ApplicationProgramYearAPIOutDTO,
-  ApplicationNumberParamAPIInDTO,
   InProgressApplicationDetailsAPIOutDTO,
   ApplicationProgressDetailsAPIOutDTO,
   EnrolmentApplicationDetailsAPIOutDTO,
   CompletedApplicationDetailsAPIOutDTO,
   ApplicationWarningsAPIOutDTO,
+  ApplicationOverallDetailsAPIOutDTO,
path: sources/packages/backend/apps/api/src/route-controllers/application/application.students.controller.ts
user: login: andrewsignori-aot
body: Removed.
created_at: 2025-03-25T21:26:49Z
author_association: COLLABORATOR
diff_hunk: @@ -547,26 +551,23 @@ export class ApplicationStudentsController extends BaseController {
   }
 
   /**
-   * Get application to request appeal.
-   ** Application eligible to be requested for
-   ** a change will be returned.
-   * @param applicationNumber
-   * @param userToken
-   * @returns application
+   * Get application to request an appeal.
+   * @param applicationId application ID.
+   * @returns application eligible to be requested for a change.
    */
   @ApiNotFoundResponse({
     description:
       "Application either not found or not eligible to request for change.",
   })
-  @Get(":applicationNumber/appeal")
+  @Get(":applicationId/appeal")
   async getApplicationToRequestAppeal(
-    @Param() applicationNumberParam: ApplicationNumberParamAPIInDTO,
+    @Param("applicationId") applicationId: number,
path: sources/packages/backend/apps/api/src/route-controllers/application/application.students.controller.ts
user: login: andrewsignori-aot
body: Added.
created_at: 2025-03-25T21:26:53Z
author_association: COLLABORATOR
diff_hunk: @@ -664,4 +665,35 @@ export class ApplicationStudentsController extends BaseController {
       },
     );
   }
+
+  /**
+   * Get application overall details for the given application.
+   * @param applicationId application Id.
+   * @returns application overall details.
+   */
+  @ApiNotFoundResponse({
+    description: "Application not found.",
+  })
+  @Get(":applicationId/overall-details")
+  async getApplicationOverallDetails(
+    @Param("applicationId", ParseIntPipe) applicationId: number,
+    @UserToken() userToken: StudentUserToken,
+  ): Promise<ApplicationOverallDetailsAPIOutDTO> {
+    const application = await this.applicationService.doesApplicationExist({
+      applicationId: applicationId,
+      studentId: userToken.studentId,
+    });
+    if (!application) {
+      throw new NotFoundException("Application not found.");
+    }
+    const applications =
+      await this.applicationService.getAllApplicationVersions(applicationId);
+    return {
+      previousVersions: applications.map((application) => ({
+        id: application.id,
+        submittedDate: application.submittedDate,
+        applicationEditStatus: application.applicationEditStatus,
+      })),
+    };
+  }
path: sources/packages/backend/apps/api/src/route-controllers/application/application.students.controller.ts
user: login: dheepak-aot
body: What is the reason for not centralizing the code with controller service? will it be part of upcoming PR?
created_at: 2025-03-25T21:28:08Z
author_association: COLLABORATOR
diff_hunk: @@ -664,4 +665,35 @@ export class ApplicationStudentsController extends BaseController {
       },
     );
   }
+
+  /**
+   * Get application overall details for the given application.
+   * @param applicationId application Id.
+   * @returns application overall details.
+   */
+  @ApiNotFoundResponse({
+    description: "Application not found.",
+  })
+  @Get(":applicationId/overall-details")
+  async getApplicationOverallDetails(
+    @Param("applicationId", ParseIntPipe) applicationId: number,
+    @UserToken() userToken: StudentUserToken,
+  ): Promise<ApplicationOverallDetailsAPIOutDTO> {
+    const application = await this.applicationService.doesApplicationExist({
+      applicationId: applicationId,
+      studentId: userToken.studentId,
+    });
+    if (!application) {
+      throw new NotFoundException("Application not found.");
+    }
+    const applications =
+      await this.applicationService.getAllApplicationVersions(applicationId);
+    return {
+      previousVersions: applications.map((application) => ({
+        id: application.id,
+        submittedDate: application.submittedDate,
+        applicationEditStatus: application.applicationEditStatus,
+      })),
+    };
+  }
path: sources/packages/backend/apps/api/src/route-controllers/application/application.students.controller.ts
user: login: andrewsignori-aot
body: No reason; I can add it to the controller service.
I will also refactor to prevent the two DB calls by removing the `doesApplicationExist`.
created_at: 2025-03-25T21:30:12Z
author_association: COLLABORATOR
diff_hunk: @@ -255,10 +255,10 @@ describe("StudentInstitutionsController(e2e)-getStudentApplicationSummary", () =
                 savedApplication2.currentAssessment.offering.studyStartDate,
               studyEndPeriod:
                 savedApplication2.currentAssessment.offering.studyEndDate,
-              applicationName: "Financial Aid Application",
path: sources/packages/backend/apps/api/src/route-controllers/student/_tests_/e2e/student.institutions.controller.getStudentApplicationSummary.e2e-spec.ts
user: login: dheepak-aot
body: 👍
created_at: 2025-03-25T21:32:44Z
author_association: COLLABORATOR
diff_hunk: @@ -210,10 +210,17 @@ export class ApplicationSummaryAPIOutDTO {
   studyStartPeriod: string;
   studyEndPeriod: string;
   id: number;
-  applicationName: string;
   status: ApplicationStatus;
   parentApplicationId: number;
+  /**
+   * Original application submission date.
+   */
   submittedDate?: Date;
+  /**
+   * Indicates if the application is able to use the
+   * change request feature. Other conditions may apply.
path: sources/packages/backend/apps/api/src/route-controllers/student/models/student.dto.ts
user: login: dheepak-aot
body: Minor. I did not get the part `Other conditions may apply.`
created_at: 2025-03-25T21:33:59Z
author_association: COLLABORATOR
diff_hunk: @@ -41,3 +41,20 @@ export function offeringBelongToProgramYear(
   // Check if the program start date belong to the program year.
   return isBetweenPeriod(offering.studyStartDate, programYear);
 }
+
+/**
+ * Check if a program year allows a change request.
+ * @param programYear program year entity model to be checked.
+ * @returns true if the program year allows a change request, false otherwise.
+ */
+export function allowApplicationChangeRequest(
+  programYear: Pick<ProgramYear, "programYear">,
path: sources/packages/backend/apps/api/src/utilities/program-year-utils.ts
user: login: dheepak-aot
body: What is the benefit of using the object with pick as there is a need for only property instead of using a string param?
created_at: 2025-03-25T21:45:21Z
author_association: COLLABORATOR
diff_hunk: @@ -41,3 +41,20 @@ export function offeringBelongToProgramYear(
   // Check if the program start date belong to the program year.
   return isBetweenPeriod(offering.studyStartDate, programYear);
 }
+
+/**
+ * Check if a program year allows a change request.
+ * @param programYear program year entity model to be checked.
+ * @returns true if the program year allows a change request, false otherwise.
+ */
+export function allowApplicationChangeRequest(
+  programYear: Pick<ProgramYear, "programYear">,
+): boolean {
+  // Existing program years that are not allowed to submit change request.
+  // This will allow any program year from 2025-2026 and beyond to submit.
+  // Once the change request is allowed for all program years, this function
path: sources/packages/backend/apps/api/src/utilities/program-year-utils.ts
user: login: dheepak-aot
body: Let me know if I am missing something here. During the grooming the idea that we discussed is to have the new change request process only from `2025-2026` considering the submitted appeals until `2024-2025` right?
created_at: 2025-03-25T21:49:13Z
author_association: COLLABORATOR
diff_hunk: @@ -557,148 +557,6 @@
         }
       ],
       "id": "ew7j7pq"
-    },
-    {
-      "label": "applicationId",
path: sources/packages/forms/src/form-definitions/studentrequestchange.json
user: login: dheepak-aot
body: 👍
created_at: 2025-03-25T21:55:45Z
author_association: COLLABORATOR
diff_hunk: @@ -664,4 +665,35 @@ export class ApplicationStudentsController extends BaseController {
       },
     );
   }
+
+  /**
+   * Get application overall details for the given application.
+   * @param applicationId application Id.
+   * @returns application overall details.
+   */
+  @ApiNotFoundResponse({
+    description: "Application not found.",
+  })
+  @Get(":applicationId/overall-details")
+  async getApplicationOverallDetails(
+    @Param("applicationId", ParseIntPipe) applicationId: number,
+    @UserToken() userToken: StudentUserToken,
+  ): Promise<ApplicationOverallDetailsAPIOutDTO> {
+    const application = await this.applicationService.doesApplicationExist({
+      applicationId: applicationId,
+      studentId: userToken.studentId,
+    });
+    if (!application) {
+      throw new NotFoundException("Application not found.");
+    }
+    const applications =
+      await this.applicationService.getAllApplicationVersions(applicationId);
+    return {
+      previousVersions: applications.map((application) => ({
+        id: application.id,
+        submittedDate: application.submittedDate,
+        applicationEditStatus: application.applicationEditStatus,
+      })),
+    };
+  }
path: sources/packages/backend/apps/api/src/route-controllers/application/application.students.controller.ts
user: login: andrewsignori-aot
body: The code was refactored to avoid the two DB queries and moved to a centralized place.
If the methods evolve differently for students and the Ministry, we can review the centralization as needed.
created_at: 2025-03-25T21:57:09Z
author_association: COLLABORATOR
diff_hunk: @@ -210,10 +210,17 @@ export class ApplicationSummaryAPIOutDTO {
   studyStartPeriod: string;
   studyEndPeriod: string;
   id: number;
-  applicationName: string;
   status: ApplicationStatus;
   parentApplicationId: number;
+  /**
+   * Original application submission date.
+   */
   submittedDate?: Date;
+  /**
+   * Indicates if the application is able to use the
+   * change request feature. Other conditions may apply.
path: sources/packages/backend/apps/api/src/route-controllers/student/models/student.dto.ts
user: login: andrewsignori-aot
body: The property itself does not check other conditions, for instance, if the application is completed.
The idea is to indicate if the application has the possibility to execute the "new change request" if all conditions are met.
created_at: 2025-03-25T21:58:55Z
author_association: COLLABORATOR
diff_hunk: @@ -41,3 +41,20 @@ export function offeringBelongToProgramYear(
   // Check if the program start date belong to the program year.
   return isBetweenPeriod(offering.studyStartDate, programYear);
 }
+
+/**
+ * Check if a program year allows a change request.
+ * @param programYear program year entity model to be checked.
+ * @returns true if the program year allows a change request, false otherwise.
+ */
+export function allowApplicationChangeRequest(
+  programYear: Pick<ProgramYear, "programYear">,
path: sources/packages/backend/apps/api/src/utilities/program-year-utils.ts
user: login: andrewsignori-aot
body: Similar to some user helpers, the idea was to provide the program year entity model and be precise about which column we are using from the model.

created_at: 2025-03-25T22:02:15Z
author_association: COLLABORATOR
diff_hunk: @@ -21,7 +21,12 @@
           :loading="loading"
           :mobile="isMobile"
           @update:options="paginationAndSortEvent"
+          show-expand
+          v-model:expanded="expandedItems"
         >
+          <template v-slot:loading>
+            <v-skeleton-loader type="table-row@5"></v-skeleton-loader>
+          </template>
           <template #[`item.applicationNumber`]="{ item }">
             {{ item.applicationNumber }}
           </template>
path: sources/packages/web/src/components/students/StudentApplicationsExtendedSummary.vue
user: login: dheepak-aot
body: Aren't we removing the column ? I thought so when I saw the `applicationName` property being removed. 
![image](https://github.com/user-attachments/assets/86e15e4a-fe52-47f1-8ad4-b1994a36c51d)

created_at: 2025-03-25T22:03:05Z
author_association: COLLABORATOR
diff_hunk: @@ -41,3 +41,20 @@ export function offeringBelongToProgramYear(
   // Check if the program start date belong to the program year.
   return isBetweenPeriod(offering.studyStartDate, programYear);
 }
+
+/**
+ * Check if a program year allows a change request.
+ * @param programYear program year entity model to be checked.
+ * @returns true if the program year allows a change request, false otherwise.
+ */
+export function allowApplicationChangeRequest(
+  programYear: Pick<ProgramYear, "programYear">,
+): boolean {
+  // Existing program years that are not allowed to submit change request.
+  // This will allow any program year from 2025-2026 and beyond to submit.
+  // Once the change request is allowed for all program years, this function
path: sources/packages/backend/apps/api/src/utilities/program-year-utils.ts
user: login: andrewsignori-aot
body: Yes, this method will return true to any program year `2025-2026` and on.
created_at: 2025-03-25T22:04:24Z
author_association: COLLABORATOR
diff_hunk: @@ -21,7 +21,12 @@
           :loading="loading"
           :mobile="isMobile"
           @update:options="paginationAndSortEvent"
+          show-expand
+          v-model:expanded="expandedItems"
         >
+          <template v-slot:loading>
+            <v-skeleton-loader type="table-row@5"></v-skeleton-loader>
+          </template>
           <template #[`item.applicationNumber`]="{ item }">
             {{ item.applicationNumber }}
           </template>
path: sources/packages/web/src/components/students/StudentApplicationsExtendedSummary.vue
user: login: andrewsignori-aot
body: Yes, removed from the headers model, the UI removed the column, and I forgot to remove it from the Vue template.
created_at: 2025-03-25T22:05:37Z
author_association: COLLABORATOR
diff_hunk: @@ -21,7 +21,12 @@
           :loading="loading"
           :mobile="isMobile"
           @update:options="paginationAndSortEvent"
+          show-expand
+          v-model:expanded="expandedItems"
         >
+          <template v-slot:loading>
+            <v-skeleton-loader type="table-row@5"></v-skeleton-loader>
+          </template>
           <template #[`item.applicationNumber`]="{ item }">
             {{ item.applicationNumber }}
           </template>
path: sources/packages/web/src/components/students/StudentApplicationsExtendedSummary.vue
user: login: andrewsignori-aot
body: It is removed now.
created_at: 2025-03-25T22:07:07Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,69 @@
+<template>
+  <v-table>
+    <thead>
+      <tr>
+        <th id="submitted-header">Submitted</th>
+        <th id="status-header">Status</th>
+        <th id="actions-header">Actions</th>
+      </tr>
+    </thead>
+    <tbody>
+      <tr v-if="loading">
+        <td colspan="3">
+          <v-skeleton-loader type="table-row@2" />
+        </td>
+      </tr>
+      <tr v-for="version in versions" :key="version.id">
+        <td headers="submitted-header">
+          {{ getISODateHourMinuteString(version.submittedDate) }}
+        </td>
+        <td headers="status-header">
+          {{
+            mapApplicationEditStatusForStudents(version.applicationEditStatus)
path: sources/packages/web/src/components/students/StudentApplicationsVersion.vue
user: login: dheepak-aot
body: Are we deferring the use of status chip to upcoming PR or upcoming tickets related to the feature?
created_at: 2025-03-25T22:23:17Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,69 @@
+<template>
+  <v-table>
+    <thead>
+      <tr>
+        <th id="submitted-header">Submitted</th>
+        <th id="status-header">Status</th>
+        <th id="actions-header">Actions</th>
+      </tr>
+    </thead>
+    <tbody>
+      <tr v-if="loading">
+        <td colspan="3">
+          <v-skeleton-loader type="table-row@2" />
+        </td>
+      </tr>
+      <tr v-for="version in versions" :key="version.id">
+        <td headers="submitted-header">
+          {{ getISODateHourMinuteString(version.submittedDate) }}
+        </td>
+        <td headers="status-header">
+          {{
+            mapApplicationEditStatusForStudents(version.applicationEditStatus)
path: sources/packages/web/src/components/students/StudentApplicationsVersion.vue
user: login: andrewsignori-aot
body: There are no requirements or decisions on status chips or how they would look like so far.
created_at: 2025-03-25T22:38:13Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,69 @@
+<template>
+  <v-table>
+    <thead>
+      <tr>
+        <th id="submitted-header">Submitted</th>
+        <th id="status-header">Status</th>
+        <th id="actions-header">Actions</th>
+      </tr>
+    </thead>
+    <tbody>
+      <tr v-if="loading">
+        <td colspan="3">
+          <v-skeleton-loader type="table-row@2" />
+        </td>
+      </tr>
+      <tr v-for="version in versions" :key="version.id">
+        <td headers="submitted-header">
+          {{ getISODateHourMinuteString(version.submittedDate) }}
+        </td>
+        <td headers="status-header">
+          {{
+            mapApplicationEditStatusForStudents(version.applicationEditStatus)
+          }}
+        </td>
+        <td headers="actions-header">
+          <v-btn
+            variant="text"
+            color="primary"
+            @click="$emit('viewApplicationVersion', version.id)"
+            >View
+            <v-tooltip activator="parent" location="start"
+              >Click to view this application version</v-tooltip
+            >
+          </v-btn>
+        </td>
+      </tr>
+    </tbody>
+  </v-table>
+</template>
+
+<script lang="ts">
+import { defineComponent } from "vue";
+import { useApplication, useFormatters } from "@/composables";
+import { ApplicationVersionAPIOutDTO } from "@/services/http/dto";
+
+export default defineComponent({
+  emits: {
+    viewApplicationVersion: (applicationId: number) => !!applicationId,
+  },
+  props: {
+    versions: {
+      type: Array<ApplicationVersionAPIOutDTO>,
+      required: false,
path: sources/packages/web/src/components/students/StudentApplicationsVersion.vue
user: login: dheepak-aot
body: Why isn't `required: true`?
created_at: 2025-03-25T22:40:12Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,69 @@
+<template>
+  <v-table>
+    <thead>
+      <tr>
+        <th id="submitted-header">Submitted</th>
+        <th id="status-header">Status</th>
+        <th id="actions-header">Actions</th>
+      </tr>
+    </thead>
+    <tbody>
+      <tr v-if="loading">
+        <td colspan="3">
+          <v-skeleton-loader type="table-row@2" />
+        </td>
+      </tr>
+      <tr v-for="version in versions" :key="version.id">
+        <td headers="submitted-header">
+          {{ getISODateHourMinuteString(version.submittedDate) }}
+        </td>
+        <td headers="status-header">
+          {{
+            mapApplicationEditStatusForStudents(version.applicationEditStatus)
+          }}
+        </td>
+        <td headers="actions-header">
+          <v-btn
+            variant="text"
+            color="primary"
+            @click="$emit('viewApplicationVersion', version.id)"
+            >View
+            <v-tooltip activator="parent" location="start"
+              >Click to view this application version</v-tooltip
+            >
+          </v-btn>
+        </td>
+      </tr>
+    </tbody>
+  </v-table>
+</template>
+
+<script lang="ts">
+import { defineComponent } from "vue";
+import { useApplication, useFormatters } from "@/composables";
+import { ApplicationVersionAPIOutDTO } from "@/services/http/dto";
+
+export default defineComponent({
+  emits: {
+    viewApplicationVersion: (applicationId: number) => !!applicationId,
+  },
+  props: {
+    versions: {
+      type: Array<ApplicationVersionAPIOutDTO>,
+      required: false,
path: sources/packages/web/src/components/students/StudentApplicationsVersion.vue
user: login: andrewsignori-aot
body: Since it is loaded on demand, it does not need to have items till the loading is completed.
I did not see a benefit to having it required.

created_at: 2025-03-25T22:42:46Z
author_association: COLLABORATOR
diff_hunk: @@ -84,31 +87,32 @@ export default defineComponent({
   components: {
     AppealRequestsForm,
   },
-  setup() {
+  props: {
+    applicationId: {
+      type: Number,
+      required: true,
+    },
+  },
+  setup(props) {
     const snackBar = useSnackBar();
     const processing = ref(false);
     const appealRequestsForms = ref([] as StudentAppealRequest[]);
-    let applicationId: number;
+    const initialData = ref({} as StudentRequestSelectedForms);
+    let applicationAppealData: ApplicationProgramYearAPIOutDTO;
     const showRequestForAppeal = computed(
       () => appealRequestsForms.value.length === 0,
     );
 
-    const submitRequest = async (
-      form: FormIOForm<StudentRequestSelectedForms>,
-    ) => {
+    onMounted(async () => {
       try {
-        const application =
+        applicationAppealData =
           await ApplicationService.shared.getApplicationForRequestChange(
-            form.data.applicationNumber,
+            props.applicationId,
           );
-        applicationId = application.id;
-        appealRequestsForms.value = form.data.formNames.map(
-          (formName) =>
-            ({
-              formName,
-              data: { programYear: application.programYear },
-            } as StudentAppealRequest),
-        );
+        initialData.value = {
+          applicationNumber: applicationAppealData.applicationNumber,
+          formNames: [],
+        };
       } catch (error: unknown) {
         const errorMessage = "An error happened while requesting a change.";
         const errorLabel = "Unexpected error";
path: sources/packages/web/src/views/student/StudentAppealRequest.vue
user: login: dheepak-aot
body: May or may not be part of the PR. Other than the unexpected error, other warning snackbars aren't relevant anymore right?
created_at: 2025-03-25T22:57:13Z
author_association: COLLABORATOR
diff_hunk: @@ -3,11 +3,11 @@
     <template #header>
       <header-navigator title="Student" subTitle="Request a Change" />
path: sources/packages/web/src/views/student/StudentAppealRequest.vue
user: login: dheepak-aot
body: Minor. Having back button to student applications would be good even if temporary to avoid confusion. 
![image](https://github.com/user-attachments/assets/7321012b-5d32-4525-a990-66eaa011cc87)

created_at: 2025-03-25T23:23:08Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,69 @@
+<template>
+  <v-table>
+    <thead>
+      <tr>
path: sources/packages/web/src/components/students/StudentApplicationsVersion.vue
user: login: dheepak-aot
body: I see that if the result set has only one item the divider line is thicker. 

![image](https://github.com/user-attachments/assets/0217722c-e80c-49d3-a3f2-c50b5f1b8430)

created_at: 2025-03-25T23:33:54Z
author_association: COLLABORATOR
diff_hunk: @@ -84,31 +87,32 @@ export default defineComponent({
   components: {
     AppealRequestsForm,
   },
-  setup() {
+  props: {
+    applicationId: {
+      type: Number,
+      required: true,
+    },
+  },
+  setup(props) {
     const snackBar = useSnackBar();
     const processing = ref(false);
     const appealRequestsForms = ref([] as StudentAppealRequest[]);
-    let applicationId: number;
+    const initialData = ref({} as StudentRequestSelectedForms);
+    let applicationAppealData: ApplicationProgramYearAPIOutDTO;
     const showRequestForAppeal = computed(
       () => appealRequestsForms.value.length === 0,
     );
 
-    const submitRequest = async (
-      form: FormIOForm<StudentRequestSelectedForms>,
-    ) => {
+    onMounted(async () => {
       try {
-        const application =
+        applicationAppealData =
           await ApplicationService.shared.getApplicationForRequestChange(
-            form.data.applicationNumber,
+            props.applicationId,
           );
-        applicationId = application.id;
-        appealRequestsForms.value = form.data.formNames.map(
-          (formName) =>
-            ({
-              formName,
-              data: { programYear: application.programYear },
-            } as StudentAppealRequest),
-        );
+        initialData.value = {
+          applicationNumber: applicationAppealData.applicationNumber,
+          formNames: [],
+        };
       } catch (error: unknown) {
         const errorMessage = "An error happened while requesting a change.";
         const errorLabel = "Unexpected error";
path: sources/packages/web/src/views/student/StudentAppealRequest.vue
user: login: andrewsignori-aot
body: `INVALID_APPLICATION_NUMBER` is now removed.
created_at: 2025-03-25T23:43:48Z
author_association: COLLABORATOR
diff_hunk: @@ -3,11 +3,11 @@
     <template #header>
       <header-navigator title="Student" subTitle="Request a Change" />
path: sources/packages/web/src/views/student/StudentAppealRequest.vue
user: login: andrewsignori-aot
body: The application does not have also, and this page can be reached from two different pages, which would demand a decision on where to return or to track the origin page. 
I do not see it as a core part of this change, but I will let the business know during the demo.
created_at: 2025-03-25T23:52:04Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,69 @@
+<template>
+  <v-table>
+    <thead>
+      <tr>
path: sources/packages/web/src/components/students/StudentApplicationsVersion.vue
user: login: andrewsignori-aot
body: I am not sure if it is thicker or just a slightly different gray.
I would prefer not to invest time in it.
created_at: 2025-03-25T23:59:02Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,69 @@
+<template>
+  <v-table>
+    <thead>
+      <tr>
path: sources/packages/web/src/components/students/StudentApplicationsVersion.vue
user: login: andrewsignori-aot
body: I can change it to a `v-data-table`, but I am not sure if it would be worth it.
created_at: 2025-03-25T23:59:58Z
author_association: COLLABORATOR
diff_hunk: @@ -181,13 +248,54 @@ export default defineComponent({
     };
 
     const emitCancel = (applicationId: number) => {
-      emit("openConfirmCancel", applicationId, () => reloadApplications());
+      emit("openConfirmCancel", applicationId, () => getStudentApplications());
+    };
+
+    const versionsExpanderClick = async (
+      application: ApplicationSummaryModel,
+      toggleExpand: () => void,
+    ) => {
+      toggleExpand();
+      if (application.versions) {
+        // Application versions are not required to be fetched every time.
+        // If they are already loaded there is no critical reason to fetch them again.
+        return;
+      }
+      try {
+        application.loadingVersions = true;
+        const applications =
+          await ApplicationService.shared.getApplicationOverallDetails(
+            application.id,
+          );
+        application.versions = applications.previousVersions;
+      } catch (error) {
+        console.error(error);
+      } finally {
+        application.loadingVersions = false;
+      }
+    };
+
+    const canDisplayEditOrCancel = (
+      application: ApplicationSummaryAPIOutDTO,
path: sources/packages/web/src/components/students/StudentApplicationsExtendedSummary.vue
user: login: sh16011993
body: only the status property of the application is required. Shouldn't we pass only the application status instead of the entire application in this case? Same for the `canDisplayChangeRequest`.
created_at: 2025-03-26T00:06:09Z
author_association: COLLABORATOR
diff_hunk: @@ -181,13 +248,54 @@ export default defineComponent({
     };
 
     const emitCancel = (applicationId: number) => {
-      emit("openConfirmCancel", applicationId, () => reloadApplications());
+      emit("openConfirmCancel", applicationId, () => getStudentApplications());
+    };
+
+    const versionsExpanderClick = async (
+      application: ApplicationSummaryModel,
+      toggleExpand: () => void,
+    ) => {
+      toggleExpand();
+      if (application.versions) {
+        // Application versions are not required to be fetched every time.
+        // If they are already loaded there is no critical reason to fetch them again.
+        return;
+      }
+      try {
+        application.loadingVersions = true;
+        const applications =
+          await ApplicationService.shared.getApplicationOverallDetails(
+            application.id,
+          );
+        application.versions = applications.previousVersions;
+      } catch (error) {
+        console.error(error);
+      } finally {
+        application.loadingVersions = false;
+      }
+    };
+
+    const canDisplayEditOrCancel = (
+      application: ApplicationSummaryAPIOutDTO,
path: sources/packages/web/src/components/students/StudentApplicationsExtendedSummary.vue
user: login: andrewsignori-aot
body: I can change this one since it is a single parameter.
IMO, from vue templates (differently from services and other methods), it is clean to pass the object.
For instance, if this method requires later 2 or three parameters from the same object, instead of passing 3 arguments from the vue template, I believe the template would look cleaner passing the object. Does it make sense?
created_at: 2025-03-26T15:34:10Z
author_association: COLLABORATOR
diff_hunk: @@ -664,4 +665,35 @@ export class ApplicationStudentsController extends BaseController {
       },
     );
   }
+
+  /**
+   * Get application overall details for the given application.
+   * @param applicationId application Id.
+   * @returns application overall details.
+   */
+  @ApiNotFoundResponse({
+    description: "Application not found.",
+  })
+  @Get(":applicationId/overall-details")
+  async getApplicationOverallDetails(
+    @Param("applicationId", ParseIntPipe) applicationId: number,
+    @UserToken() userToken: StudentUserToken,
+  ): Promise<ApplicationOverallDetailsAPIOutDTO> {
+    const application = await this.applicationService.doesApplicationExist({
+      applicationId: applicationId,
+      studentId: userToken.studentId,
+    });
+    if (!application) {
+      throw new NotFoundException("Application not found.");
+    }
+    const applications =
+      await this.applicationService.getAllApplicationVersions(applicationId);
+    return {
+      previousVersions: applications.map((application) => ({
+        id: application.id,
+        submittedDate: application.submittedDate,
+        applicationEditStatus: application.applicationEditStatus,
+      })),
+    };
+  }
path: sources/packages/backend/apps/api/src/route-controllers/application/application.students.controller.ts
user: login: dheepak-aot
body: Thanks for the refactor.
created_at: 2025-03-26T16:16:11Z
author_association: COLLABORATOR
diff_hunk: @@ -210,10 +210,17 @@ export class ApplicationSummaryAPIOutDTO {
   studyStartPeriod: string;
   studyEndPeriod: string;
   id: number;
-  applicationName: string;
   status: ApplicationStatus;
   parentApplicationId: number;
+  /**
+   * Original application submission date.
+   */
   submittedDate?: Date;
+  /**
+   * Indicates if the application is able to use the
+   * change request feature. Other conditions may apply.
path: sources/packages/backend/apps/api/src/route-controllers/student/models/student.dto.ts
user: login: dheepak-aot
body: Got it. Thanks. Minor and in that case, i would recommend the name `isChangeRequestAllowedForPY` unless we have an idea to not expand the logic for the field. Still just a suggestion and not a blocker.
created_at: 2025-03-26T16:21:42Z
author_association: COLLABORATOR
diff_hunk: @@ -41,3 +41,20 @@ export function offeringBelongToProgramYear(
   // Check if the program start date belong to the program year.
   return isBetweenPeriod(offering.studyStartDate, programYear);
 }
+
+/**
+ * Check if a program year allows a change request.
+ * @param programYear program year entity model to be checked.
+ * @returns true if the program year allows a change request, false otherwise.
+ */
+export function allowApplicationChangeRequest(
+  programYear: Pick<ProgramYear, "programYear">,
+): boolean {
+  // Existing program years that are not allowed to submit change request.
+  // This will allow any program year from 2025-2026 and beyond to submit.
+  // Once the change request is allowed for all program years, this function
path: sources/packages/backend/apps/api/src/utilities/program-year-utils.ts
user: login: dheepak-aot
body: Did you mean, from when we have only PY starting from `2025-2025` to be active this method must be removed?
created_at: 2025-03-26T16:23:54Z
author_association: COLLABORATOR
diff_hunk: @@ -49,38 +43,93 @@
             <status-chip-application :status="item.status" />
           </template>
           <template #[`item.actions`]="{ item }">
-            <span
-              v-if="
-                !(
-                  item.status === ApplicationStatus.Cancelled ||
-                  item.status === ApplicationStatus.Completed
-                )
-              "
-            >
+            <v-btn-group variant="text" :rounded="false">
+              <v-btn color="primary" @click="$emit('goToApplication', item.id)"
+                >View
+                <v-tooltip activator="parent" location="start"
+                  >Click to view this application</v-tooltip
+                >
+              </v-btn>
               <v-btn
+                v-if="canDisplayEditOrCancel(item)"
                 :disabled="!hasSINValidStatus"
-                variant="plain"
                 color="primary"
-                class="label-bold"
                 @click="$emit('editApplicationAction', item.status, item.id)"
                 append-icon="mdi-pencil-outline"
-                ><span class="label-bold">Edit</span>
+                >Edit
                 <v-tooltip activator="parent" location="start"
                   >Click to edit this application</v-tooltip
                 >
               </v-btn>
               <v-btn
+                v-if="canDisplayChangeRequest(item)"
+                :disabled="!hasSINValidStatus"
+                color="primary"
+                @click="
+                  $emit(
+                    'changeApplicationAction',
+                    item.id,
+                    item.isChangeRequestAllowed,
+                  )
+                "
+                append-icon="mdi-pencil-outline"
+                >Change Request
+                <v-tooltip activator="parent" location="start"
+                  >Click to request a change in this application</v-tooltip
+                >
+              </v-btn>
+              <v-btn
+                v-if="canDisplayEditOrCancel(item)"
                 :disabled="!hasSINValidStatus"
-                variant="plain"
                 color="primary"
-                class="label-bold"
                 @click="emitCancel(item.id)"
-                ><span class="label-bold">Cancel</span>
+                >Cancel
                 <v-tooltip activator="parent" location="start"
                   >Click to cancel this application</v-tooltip
                 >
               </v-btn>
-            </span>
+            </v-btn-group>
+          </template>
+          <template
+            v-slot:[`item.data-table-expand`]="{
path: sources/packages/web/src/components/students/StudentApplicationsExtendedSummary.vue
user: login: sh16011993
body: We usually follow `#` instead of `v-slot:`, although I see there are a few `v-slot:` in the codebase.
created_at: 2025-03-26T16:27:46Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,69 @@
+<template>
+  <v-table>
+    <thead>
+      <tr>
+        <th id="submitted-header">Submitted</th>
+        <th id="status-header">Status</th>
+        <th id="actions-header">Actions</th>
+      </tr>
+    </thead>
+    <tbody>
+      <tr v-if="loading">
+        <td colspan="3">
+          <v-skeleton-loader type="table-row@2" />
+        </td>
+      </tr>
+      <tr v-for="version in versions" :key="version.id">
+        <td headers="submitted-header">
+          {{ getISODateHourMinuteString(version.submittedDate) }}
+        </td>
+        <td headers="status-header">
+          {{
+            mapApplicationEditStatusForStudents(version.applicationEditStatus)
path: sources/packages/web/src/components/students/StudentApplicationsVersion.vue
user: login: dheepak-aot
body: Yes, but showing any sttatus using Chip is our UI standard, so it would be good to have that included in any of the upcoming tickets will be required to follow the same.
created_at: 2025-03-26T16:30:33Z
author_association: COLLABORATOR
diff_hunk: @@ -181,13 +248,54 @@ export default defineComponent({
     };
 
     const emitCancel = (applicationId: number) => {
-      emit("openConfirmCancel", applicationId, () => reloadApplications());
+      emit("openConfirmCancel", applicationId, () => getStudentApplications());
+    };
+
+    const versionsExpanderClick = async (
+      application: ApplicationSummaryModel,
+      toggleExpand: () => void,
+    ) => {
+      toggleExpand();
+      if (application.versions) {
+        // Application versions are not required to be fetched every time.
+        // If they are already loaded there is no critical reason to fetch them again.
+        return;
+      }
+      try {
+        application.loadingVersions = true;
+        const applications =
+          await ApplicationService.shared.getApplicationOverallDetails(
+            application.id,
+          );
+        application.versions = applications.previousVersions;
+      } catch (error) {
+        console.error(error);
+      } finally {
+        application.loadingVersions = false;
+      }
+    };
+
+    const canDisplayEditOrCancel = (
+      application: ApplicationSummaryAPIOutDTO,
path: sources/packages/web/src/components/students/StudentApplicationsExtendedSummary.vue
user: login: sh16011993
body: Yes, got it. 👍 
created_at: 2025-03-26T18:09:07Z
author_association: COLLABORATOR
diff_hunk: @@ -210,10 +210,17 @@ export class ApplicationSummaryAPIOutDTO {
   studyStartPeriod: string;
   studyEndPeriod: string;
   id: number;
-  applicationName: string;
   status: ApplicationStatus;
   parentApplicationId: number;
+  /**
+   * Original application submission date.
+   */
   submittedDate?: Date;
+  /**
+   * Indicates if the application is able to use the
+   * change request feature. Other conditions may apply.
path: sources/packages/backend/apps/api/src/route-controllers/student/models/student.dto.ts
user: login: andrewsignori-aot
body: Renamed.
created_at: 2025-03-26T18:49:08Z
author_association: COLLABORATOR
diff_hunk: @@ -41,3 +41,20 @@ export function offeringBelongToProgramYear(
   // Check if the program start date belong to the program year.
   return isBetweenPeriod(offering.studyStartDate, programYear);
 }
+
+/**
+ * Check if a program year allows a change request.
+ * @param programYear program year entity model to be checked.
+ * @returns true if the program year allows a change request, false otherwise.
+ */
+export function allowApplicationChangeRequest(
+  programYear: Pick<ProgramYear, "programYear">,
+): boolean {
+  // Existing program years that are not allowed to submit change request.
+  // This will allow any program year from 2025-2026 and beyond to submit.
+  // Once the change request is allowed for all program years, this function
path: sources/packages/backend/apps/api/src/utilities/program-year-utils.ts
user: login: andrewsignori-aot
body: Yes, when all active program years are from `2025-2026` on, we can have it removed.
created_at: 2025-03-26T18:50:06Z
author_association: COLLABORATOR
diff_hunk: @@ -49,38 +43,93 @@
             <status-chip-application :status="item.status" />
           </template>
           <template #[`item.actions`]="{ item }">
-            <span
-              v-if="
-                !(
-                  item.status === ApplicationStatus.Cancelled ||
-                  item.status === ApplicationStatus.Completed
-                )
-              "
-            >
+            <v-btn-group variant="text" :rounded="false">
+              <v-btn color="primary" @click="$emit('goToApplication', item.id)"
+                >View
+                <v-tooltip activator="parent" location="start"
+                  >Click to view this application</v-tooltip
+                >
+              </v-btn>
               <v-btn
+                v-if="canDisplayEditOrCancel(item)"
                 :disabled="!hasSINValidStatus"
-                variant="plain"
                 color="primary"
-                class="label-bold"
                 @click="$emit('editApplicationAction', item.status, item.id)"
                 append-icon="mdi-pencil-outline"
-                ><span class="label-bold">Edit</span>
+                >Edit
                 <v-tooltip activator="parent" location="start"
                   >Click to edit this application</v-tooltip
                 >
               </v-btn>
               <v-btn
+                v-if="canDisplayChangeRequest(item)"
+                :disabled="!hasSINValidStatus"
+                color="primary"
+                @click="
+                  $emit(
+                    'changeApplicationAction',
+                    item.id,
+                    item.isChangeRequestAllowed,
+                  )
+                "
+                append-icon="mdi-pencil-outline"
+                >Change Request
+                <v-tooltip activator="parent" location="start"
+                  >Click to request a change in this application</v-tooltip
+                >
+              </v-btn>
+              <v-btn
+                v-if="canDisplayEditOrCancel(item)"
                 :disabled="!hasSINValidStatus"
-                variant="plain"
                 color="primary"
-                class="label-bold"
                 @click="emitCancel(item.id)"
-                ><span class="label-bold">Cancel</span>
+                >Cancel
                 <v-tooltip activator="parent" location="start"
                   >Click to cancel this application</v-tooltip
                 >
               </v-btn>
-            </span>
+            </v-btn-group>
+          </template>
+          <template
+            v-slot:[`item.data-table-expand`]="{
path: sources/packages/web/src/components/students/StudentApplicationsExtendedSummary.vue
user: login: andrewsignori-aot
body: Changed.
created_at: 2025-03-26T18:53:59Z
author_association: COLLABORATOR

PR comments:
user: login: dheepak-aot
created_at: 2025-03-25T23:35:06Z
body: Great work. I am totally for the skeleton loaders. It looks great.
author_association: COLLABORATOR
user: login: sonarqubecloud[bot]
created_at: 2025-03-26T18:55:16Z
body: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=bcgov_SIMS&pullRequest=4526) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=bcgov_SIMS&pullRequest=4526&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=bcgov_SIMS&pullRequest=4526&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=bcgov_SIMS&pullRequest=4526&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=bcgov_SIMS&pullRequest=4526&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.2% Duplication on New Code](https://sonarcloud.io/component_measures?id=bcgov_SIMS&pullRequest=4526&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=bcgov_SIMS&pullRequest=4526)


author_association: NONE
user: login: github-actions[bot]
created_at: 2025-03-26T18:55:43Z
body: <!-- coverage-monitor-action: E2E Workflow Workers Coverage Report -->
## E2E Workflow Workers Coverage Report

|  Totals | ![Coverage](https://img.shields.io/static/v1?label=coverage&message=69%&color=green) |
| :-- | :-- |
| Statements: | 65.81% ( 614 / 933 ) |
| Methods: | 60.36% ( 67 / 111 ) |
| Lines: | 68.87% ( 489 / 710 ) |
| Branches: | 51.79% ( 58 / 112 ) |

author_association: NONE
user: login: github-actions[bot]
created_at: 2025-03-26T18:55:45Z
body: <!-- coverage-monitor-action: Backend Unit Tests Coverage Report -->
## Backend Unit Tests Coverage Report

|  Totals | ![Coverage](https://img.shields.io/static/v1?label=coverage&message=25%&color=yellow) |
| :-- | :-- |
| Statements: | 21.98% ( 3975 / 18086 ) |
| Methods: | 9.84% ( 228 / 2317 ) |
| Lines: | 25.38% ( 3437 / 13543 ) |
| Branches: | 13.93% ( 310 / 2226 ) |

author_association: NONE
user: login: github-actions[bot]
created_at: 2025-03-26T18:58:06Z
body: <!-- coverage-monitor-action: E2E Queue Consumers Coverage Report -->
## E2E Queue Consumers Coverage Report

|  Totals | ![Coverage](https://img.shields.io/static/v1?label=coverage&message=88%&color=green) |
| :-- | :-- |
| Statements: | 85.72% ( 1507 / 1758 ) |
| Methods: | 83.25% ( 169 / 203 ) |
| Lines: | 88.1% ( 1244 / 1412 ) |
| Branches: | 65.73% ( 94 / 143 ) |

author_association: NONE
user: login: github-actions[bot]
created_at: 2025-03-26T18:59:11Z
body: <!-- coverage-monitor-action: E2E SIMS API Coverage Report -->
## E2E SIMS API Coverage Report

|  Totals | ![Coverage](https://img.shields.io/static/v1?label=coverage&message=74%&color=green) |
| :-- | :-- |
| Statements: | 70.77% ( 6513 / 9203 ) |
| Methods: | 68.62% ( 809 / 1179 ) |
| Lines: | 74.3% ( 5057 / 6806 ) |
| Branches: | 53.12% ( 647 / 1218 ) |

author_association: NONE