diff --git a/sources/packages/backend/apps/api/src/route-controllers/application-offering-change-request/_tests_/e2e/application-offering-change-request.institutions.controller.getCompletedApplications.e2e-spec.ts b/sources/packages/backend/apps/api/src/route-controllers/application-offering-change-request/_tests_/e2e/application-offering-change-request.institutions.controller.getCompletedApplications.e2e-spec.ts index 26e0a84e59..b439ee9402 100644 --- a/sources/packages/backend/apps/api/src/route-controllers/application-offering-change-request/_tests_/e2e/application-offering-change-request.institutions.controller.getCompletedApplications.e2e-spec.ts +++ b/sources/packages/backend/apps/api/src/route-controllers/application-offering-change-request/_tests_/e2e/application-offering-change-request.institutions.controller.getCompletedApplications.e2e-spec.ts @@ -150,6 +150,7 @@ describe("ApplicationOfferingChangeRequestInstitutionsController(e2e)-getComplet .expect({ results: [ { + id: declinedBySABCApplicationOfferingChange.id, applicationNumber: applicationWithDeclinedBySABCApplicationOfferingChange.applicationNumber, studyStartDate: @@ -168,6 +169,7 @@ describe("ApplicationOfferingChangeRequestInstitutionsController(e2e)-getComplet declinedBySABCApplicationOfferingChange.assessedDate.toISOString(), }, { + id: declinedByStudentApplicationOfferingChange.id, applicationNumber: applicationWithDeclinedByStudentApplicationOfferingChange.applicationNumber, studyStartDate: @@ -186,6 +188,7 @@ describe("ApplicationOfferingChangeRequestInstitutionsController(e2e)-getComplet declinedByStudentApplicationOfferingChange.studentActionDate.toISOString(), }, { + id: approvedApplicationOfferingChange.id, applicationNumber: applicationWithApprovedApplicationOfferingChange.applicationNumber, studyStartDate: @@ -312,6 +315,7 @@ describe("ApplicationOfferingChangeRequestInstitutionsController(e2e)-getComplet .expect({ results: [ { + id: approvedApplicationOfferingChange.id, applicationNumber: applicationWithApprovedApplicationOfferingChange.applicationNumber, studyStartDate: @@ -329,6 +333,7 @@ describe("ApplicationOfferingChangeRequestInstitutionsController(e2e)-getComplet approvedApplicationOfferingChange.assessedDate.toISOString(), }, { + id: declinedBySABCApplicationOfferingChange.id, applicationNumber: applicationWithDeclinedBySABCApplicationOfferingChange.applicationNumber, studyStartDate: diff --git a/sources/packages/backend/apps/api/src/route-controllers/application-offering-change-request/_tests_/e2e/application-offering-change-request.institutions.controller.getInProgressApplications.e2e-spec.ts b/sources/packages/backend/apps/api/src/route-controllers/application-offering-change-request/_tests_/e2e/application-offering-change-request.institutions.controller.getInProgressApplications.e2e-spec.ts index e8f95523d3..7aa965b6f3 100644 --- a/sources/packages/backend/apps/api/src/route-controllers/application-offering-change-request/_tests_/e2e/application-offering-change-request.institutions.controller.getInProgressApplications.e2e-spec.ts +++ b/sources/packages/backend/apps/api/src/route-controllers/application-offering-change-request/_tests_/e2e/application-offering-change-request.institutions.controller.getInProgressApplications.e2e-spec.ts @@ -121,6 +121,7 @@ describe("ApplicationOfferingChangeRequestInstitutionsController(e2e)-getInProgr .expect({ results: [ { + id: inProgressWithSABCApplicationOfferingChange.id, applicationNumber: applicationWithInProgressWithSABCApplicationOfferingChange.applicationNumber, studyStartDate: @@ -137,6 +138,7 @@ describe("ApplicationOfferingChangeRequestInstitutionsController(e2e)-getInProgr inProgressWithSABCApplicationOfferingChange.applicationOfferingChangeRequestStatus, }, { + id: inProgressWithStudentApplicationOfferingChange.id, applicationNumber: applicationWithInProgressWithStudentApplicationOfferingChange.applicationNumber, studyStartDate: @@ -203,6 +205,7 @@ describe("ApplicationOfferingChangeRequestInstitutionsController(e2e)-getInProgr .expect({ results: [ { + id: inProgressWithSABCApplicationOfferingChange.id, applicationNumber: applicationWithInProgressWithSABCApplicationOfferingChange.applicationNumber, studyStartDate: diff --git a/sources/packages/backend/apps/api/src/route-controllers/application-offering-change-request/application-offering-change-request.institutions.controller.ts b/sources/packages/backend/apps/api/src/route-controllers/application-offering-change-request/application-offering-change-request.institutions.controller.ts index 68684744de..b1b115c7f9 100644 --- a/sources/packages/backend/apps/api/src/route-controllers/application-offering-change-request/application-offering-change-request.institutions.controller.ts +++ b/sources/packages/backend/apps/api/src/route-controllers/application-offering-change-request/application-offering-change-request.institutions.controller.ts @@ -1,7 +1,20 @@ -import { Controller, Get, Param, ParseIntPipe, Query } from "@nestjs/common"; -import { ApiTags } from "@nestjs/swagger"; +import { + Body, + Controller, + Get, + NotFoundException, + Param, + ParseIntPipe, + Post, + Query, +} from "@nestjs/common"; +import { ApiNotFoundResponse, ApiTags } from "@nestjs/swagger"; import { AuthorizedParties } from "../../auth/authorized-parties.enum"; -import { AllowAuthorizedParty, HasLocationAccess } from "../../auth/decorators"; +import { + AllowAuthorizedParty, + HasLocationAccess, + UserToken, +} from "../../auth/decorators"; import { ClientTypeBaseRoute } from "../../types"; import { getUserFullName } from "../../utilities"; import { PaginatedResultsAPIOutDTO } from "../models/pagination.dto"; @@ -13,9 +26,14 @@ import { OfferingChangePaginationOptionsAPIInDTO, InProgressOfferingChangePaginationOptionsAPIInDTO, CompletedOfferingChangePaginationOptionsAPIInDTO, + ApplicationOfferingChangesAPIOutDTO, + ApplicationOfferingChangeSummaryDetailAPIOutDTO, + CreateApplicationOfferingChangeRequestAPIInDTO, } from "./models/application-offering-change-request.institutions.dto"; import { ApplicationOfferingChangeRequestService } from "../../services"; import { ApplicationOfferingChangeRequestStatus } from "@sims/sims-db"; +import { PrimaryIdentifierAPIOutDTO } from "../models/primary.identifier.dto"; +import { IInstitutionUserToken } from "../../auth"; /** * Application offering change request controller for institutions client. @@ -51,7 +69,7 @@ export class ApplicationOfferingChangeRequestInstitutionsController extends Base const applications = await this.applicationOfferingChangeRequestService.getEligibleApplications( locationId, - pagination, + { pagination }, ); return { results: applications.results.map((eachApplication) => { @@ -68,6 +86,42 @@ export class ApplicationOfferingChangeRequestInstitutionsController extends Base }; } + /** + * Gets an eligible application that can be requested for application + * offering change. + * @param locationId location id. + * @param applicationId application id. + * @returns eligible application. + */ + @ApiNotFoundResponse({ + description: "Application not found or it is not eligible.", + }) + @Get("available/application/:applicationId") + async getEligibleApplication( + @Param("locationId", ParseIntPipe) locationId: number, + @Param("applicationId", ParseIntPipe) applicationId: number, + ): Promise { + const application = + await this.applicationOfferingChangeRequestService.getEligibleApplications( + locationId, + { applicationId }, + ); + if (!application) { + throw new NotFoundException( + "Application not found or it is not eligible.", + ); + } + return { + applicationNumber: application.applicationNumber, + programId: application.currentAssessment.offering.educationProgram.id, + offeringId: application.currentAssessment.offering.id, + offeringIntensity: + application.currentAssessment.offering.offeringIntensity, + programYearId: application.programYear.id, + fullName: getUserFullName(application.student.user), + }; + } + /** * Get all in progress application offering change requests. * @param locationId location id. @@ -95,6 +149,7 @@ export class ApplicationOfferingChangeRequestInstitutionsController extends Base const offering = eachOfferingChange.application.currentAssessment.offering; return { + id: eachOfferingChange.id, applicationNumber: eachOfferingChange.application.applicationNumber, applicationId: eachOfferingChange.application.id, studyStartDate: offering.studyStartDate, @@ -137,6 +192,7 @@ export class ApplicationOfferingChangeRequestInstitutionsController extends Base const offering = eachOfferingChange.application.currentAssessment.offering; return { + id: eachOfferingChange.id, applicationNumber: eachOfferingChange.application.applicationNumber, applicationId: eachOfferingChange.application.id, studyStartDate: offering.studyStartDate, @@ -153,4 +209,70 @@ export class ApplicationOfferingChangeRequestInstitutionsController extends Base count: offeringChange.count, }; } + + /** + * Gets the Application Offering Change Request details. + * @param applicationOfferingChangeRequestId the Application Offering Change Request id. + * @param locationId location id. + * @returns Application Offering Change Request details. + */ + @Get(":applicationOfferingChangeRequestId") + @ApiNotFoundResponse({ + description: "Not able to find an Application Offering Change Request.", + }) + async getById( + @Param("applicationOfferingChangeRequestId", ParseIntPipe) + applicationOfferingChangeRequestId: number, + @Param("locationId", ParseIntPipe) locationId: number, + ): Promise { + const request = await this.applicationOfferingChangeRequestService.getById( + applicationOfferingChangeRequestId, + { locationId }, + ); + if (!request) { + throw new NotFoundException( + "Not able to find an Application Offering Change Request.", + ); + } + return { + id: request.id, + status: request.applicationOfferingChangeRequestStatus, + applicationId: request.application.id, + applicationNumber: request.application.applicationNumber, + locationName: request.application.location.name, + activeOfferingId: request.activeOffering.id, + requestedOfferingId: request.requestedOffering.id, + requestedOfferingDescription: request.requestedOffering.name, + requestedOfferingProgramId: request.requestedOffering.educationProgram.id, + requestedOfferingProgramName: + request.requestedOffering.educationProgram.name, + reason: request.reason, + assessedNoteDescription: request.assessedNote?.description, + studentFullName: getUserFullName(request.application.student.user), + }; + } + + /** + * Creates a new application offering change request. + * @param locationId location id. + * @param payload information to create the new request. + * @returns newly change request id created. + */ + @Post() + async createApplicationOfferingChangeRequest( + @UserToken() userToken: IInstitutionUserToken, + @Param("locationId", ParseIntPipe) locationId: number, + @Body() payload: CreateApplicationOfferingChangeRequestAPIInDTO, + ): Promise { + // TODO: Apply the same validations from PIR. + const applicationOfferingChangeRequest = + await this.applicationOfferingChangeRequestService.createRequest( + locationId, + payload.applicationId, + payload.offeringId, + payload.reason, + userToken.userId, + ); + return { id: applicationOfferingChangeRequest.id }; + } } diff --git a/sources/packages/backend/apps/api/src/route-controllers/application-offering-change-request/models/application-offering-change-request.institutions.dto.ts b/sources/packages/backend/apps/api/src/route-controllers/application-offering-change-request/models/application-offering-change-request.institutions.dto.ts index 44aa1ba8d8..99d89757c4 100644 --- a/sources/packages/backend/apps/api/src/route-controllers/application-offering-change-request/models/application-offering-change-request.institutions.dto.ts +++ b/sources/packages/backend/apps/api/src/route-controllers/application-offering-change-request/models/application-offering-change-request.institutions.dto.ts @@ -1,5 +1,15 @@ -import { ApplicationOfferingChangeRequestStatus } from "@sims/sims-db"; -import { IsIn, IsOptional } from "class-validator"; +import { + ApplicationOfferingChangeRequestStatus, + OfferingIntensity, + REASON_MAX_LENGTH, +} from "@sims/sims-db"; +import { + IsIn, + IsNotEmpty, + IsOptional, + IsPositive, + MaxLength, +} from "class-validator"; import { PaginationOptionsAPIInDTO } from "../../models/pagination.dto"; /** @@ -13,10 +23,23 @@ export class ApplicationOfferingChangeSummaryAPIOutDTO { fullName: string; } +/** + * Applications details for an eligible application to request an offering change. + */ +export class ApplicationOfferingChangeSummaryDetailAPIOutDTO { + applicationNumber: string; + programId: number; + offeringId: number; + offeringIntensity: OfferingIntensity; + programYearId: number; + fullName: string; +} + /** * In progress application offering change details. */ export class InProgressApplicationOfferingChangesAPIOutDTO { + id: number; applicationNumber: string; applicationId: number; studyStartDate: string; @@ -29,6 +52,7 @@ export class InProgressApplicationOfferingChangesAPIOutDTO { * Completed application offering change details. */ export class CompletedApplicationOfferingChangesAPIOutDTO { + id: number; applicationNumber: string; applicationId: number; studyStartDate: string; @@ -64,3 +88,35 @@ export class InProgressOfferingChangePaginationOptionsAPIInDTO extends Paginatio @IsIn(["applicationNumber", "fullName"]) sortField?: string; } + +/** + * Application Offering Change Request details. + */ +export class ApplicationOfferingChangesAPIOutDTO { + id: number; + status: ApplicationOfferingChangeRequestStatus; + applicationId: number; + applicationNumber: string; + locationName: string; + activeOfferingId: number; + requestedOfferingId: number; + requestedOfferingDescription: string; + requestedOfferingProgramId: number; + requestedOfferingProgramName: string; + reason?: string; + assessedNoteDescription?: string; + studentFullName: string; +} + +/** + * Information provided by the institution to create a new Application Offering Change Request. + */ +export class CreateApplicationOfferingChangeRequestAPIInDTO { + @IsPositive() + applicationId: number; + @IsPositive() + offeringId: number; + @IsNotEmpty() + @MaxLength(REASON_MAX_LENGTH) + reason: string; +} diff --git a/sources/packages/backend/apps/api/src/route-controllers/education-program-offering/education-program-offering.aest.controller.ts b/sources/packages/backend/apps/api/src/route-controllers/education-program-offering/education-program-offering.aest.controller.ts index 87bdeec96b..4aa344cbdf 100644 --- a/sources/packages/backend/apps/api/src/route-controllers/education-program-offering/education-program-offering.aest.controller.ts +++ b/sources/packages/backend/apps/api/src/route-controllers/education-program-offering/education-program-offering.aest.controller.ts @@ -185,7 +185,7 @@ export class EducationProgramOfferingAESTController extends BaseController { ): Promise { const offering = await this.programOfferingService.getOfferingById( offeringId, - true, + { isPrecedingOffering: true }, ); if (!offering) { throw new NotFoundException("Offering not found."); diff --git a/sources/packages/backend/apps/api/src/route-controllers/education-program-offering/education-program-offering.controller.service.ts b/sources/packages/backend/apps/api/src/route-controllers/education-program-offering/education-program-offering.controller.service.ts index 05cb4b87cf..c5fa79c72d 100644 --- a/sources/packages/backend/apps/api/src/route-controllers/education-program-offering/education-program-offering.controller.service.ts +++ b/sources/packages/backend/apps/api/src/route-controllers/education-program-offering/education-program-offering.controller.service.ts @@ -27,7 +27,11 @@ import { OfferingValidationModel, CreateFromValidatedOfferingError, } from "../../services"; -import { getOfferingNameAndPeriod, getUserFullName } from "../../utilities"; +import { + deliveryMethod, + getOfferingNameAndPeriod, + getUserFullName, +} from "../../utilities"; import { OptionItemAPIOutDTO } from "../models/common.dto"; import { OfferingsPaginationOptionsAPIInDTO, @@ -37,6 +41,7 @@ import { EducationProgramOfferingAPIInDTO, EducationProgramOfferingAPIOutDTO, EducationProgramOfferingSummaryAPIOutDTO, + EducationProgramOfferingSummaryViewAPIOutDTO, OfferingBulkInsertValidationResultAPIOutDTO, } from "./models/education-program-offering.dto"; @@ -45,6 +50,7 @@ export class EducationProgramOfferingControllerService { constructor( private readonly offeringService: EducationProgramOfferingService, private readonly programService: EducationProgramService, + private readonly programOfferingService: EducationProgramOfferingService, ) {} /** @@ -338,4 +344,57 @@ export class EducationProgramOfferingControllerService { ), }; } + + /** + * Gets the offering simplified details, not including, for instance, + * validations, approvals and extensive data. + * Useful to have an overview of the offering details, for instance, + * when the user needs need to have quick access to data in order to + * support operations like confirmation of enrolment or scholastic + * standing requests or offering change request. + * @param offeringId offering id. + * @param options method options: + * - `locationId`: location for authorization. + * @returns education program offering. + */ + async getOfferingById( + offeringId: number, + options?: { + locationId?: number; + }, + ): Promise { + const offering = await this.programOfferingService.getOfferingById( + offeringId, + { locationId: options?.locationId }, + ); + if (!offering) { + throw new NotFoundException( + "Not able to find the Education Program offering.", + ); + } + const program = offering.educationProgram; + return { + id: offering.id, + offeringName: offering.name, + studyStartDate: offering.studyStartDate, + studyEndDate: offering.studyEndDate, + actualTuitionCosts: offering.actualTuitionCosts, + programRelatedCosts: offering.programRelatedCosts, + mandatoryFees: offering.mandatoryFees, + exceptionalExpenses: offering.exceptionalExpenses, + offeringDelivered: offering.offeringDelivered, + lacksStudyBreaks: offering.lacksStudyBreaks, + offeringIntensity: offering.offeringIntensity, + studyBreaks: offering.studyBreaks?.studyBreaks, + locationName: offering.institutionLocation.name, + programId: program.id, + programName: program.name, + programDescription: program.description, + programCredential: program.credentialType, + programDelivery: deliveryMethod( + program.deliveredOnline, + program.deliveredOnSite, + ), + }; + } } diff --git a/sources/packages/backend/apps/api/src/route-controllers/education-program-offering/education-program-offering.institutions.controller.ts b/sources/packages/backend/apps/api/src/route-controllers/education-program-offering/education-program-offering.institutions.controller.ts index 2a67901946..b11ac0d243 100644 --- a/sources/packages/backend/apps/api/src/route-controllers/education-program-offering/education-program-offering.institutions.controller.ts +++ b/sources/packages/backend/apps/api/src/route-controllers/education-program-offering/education-program-offering.institutions.controller.ts @@ -51,6 +51,7 @@ import { EducationProgramOfferingAPIOutDTO, EducationProgramOfferingBasicDataAPIInDTO, EducationProgramOfferingSummaryAPIOutDTO, + EducationProgramOfferingSummaryViewAPIOutDTO, OfferingValidationResultAPIOutDTO, } from "./models/education-program-offering.dto"; import { @@ -580,4 +581,30 @@ export class EducationProgramOfferingInstitutionsController extends BaseControll throw error; } } + + /** + * Gets the offering simplified details, not including, for instance, + * validations, approvals and extensive data. + * Useful to have an overview of the offering details, for instance, + * when the user needs need to have quick access to data in order to + * support operations like confirmation of enrolment or scholastic + * standing requests or offering change request. + * @param offeringId offering. + * @param locationId offering location. + * @returns offering details. + */ + @HasLocationAccess("locationId") + @ApiNotFoundResponse({ + description: "Not able to find the Education Program offering.", + }) + @Get("location/:locationId/offering/:offeringId/summary-details") + async getOfferingSummaryDetailsById( + @Param("offeringId", ParseIntPipe) offeringId: number, + @Param("locationId", ParseIntPipe) locationId: number, + ): Promise { + return this.educationProgramOfferingControllerService.getOfferingById( + offeringId, + { locationId }, + ); + } } diff --git a/sources/packages/backend/apps/api/src/route-controllers/education-program-offering/models/education-program-offering.dto.ts b/sources/packages/backend/apps/api/src/route-controllers/education-program-offering/models/education-program-offering.dto.ts index 4b27bde1d6..5972536db2 100644 --- a/sources/packages/backend/apps/api/src/route-controllers/education-program-offering/models/education-program-offering.dto.ts +++ b/sources/packages/backend/apps/api/src/route-controllers/education-program-offering/models/education-program-offering.dto.ts @@ -130,6 +130,27 @@ export class EducationProgramOfferingAPIOutDTO { validationInfos: string[]; } +export class EducationProgramOfferingSummaryViewAPIOutDTO { + id: number; + offeringName: string; + studyStartDate: string; + studyEndDate: string; + actualTuitionCosts: number; + programRelatedCosts: number; + mandatoryFees: number; + exceptionalExpenses: number; + offeringDelivered: string; + lacksStudyBreaks: boolean; + offeringIntensity: OfferingIntensity; + studyBreaks?: StudyBreakAPIOutDTO[]; + locationName: string; + programId: number; + programName: string; + programDescription: string; + programCredential: string; + programDelivery: string; +} + export class EducationProgramOfferingSummaryAPIOutDTO { id: number; name: string; diff --git a/sources/packages/backend/apps/api/src/services/application-offering-change-request/application-offering-change-request.service.ts b/sources/packages/backend/apps/api/src/services/application-offering-change-request/application-offering-change-request.service.ts index b5892c9ba0..64545ce8d7 100644 --- a/sources/packages/backend/apps/api/src/services/application-offering-change-request/application-offering-change-request.service.ts +++ b/sources/packages/backend/apps/api/src/services/application-offering-change-request/application-offering-change-request.service.ts @@ -5,6 +5,8 @@ import { ApplicationOfferingChangeRequest, ApplicationOfferingChangeRequestStatus, ApplicationStatus, + EducationProgramOffering, + User, getUserFullNameLikeSearch, transformToApplicationEntitySortField, } from "@sims/sims-db"; @@ -20,60 +22,101 @@ export class ApplicationOfferingChangeRequestService { private readonly applicationOfferingChangeRequestRepo: Repository, ) {} + /** + * Gets an eligible application that can be requested for application + * offering change. + * @param locationId location id. + * @param options method options: + * - `applicationId` specific eligible application to be retrieved. + * @returns eligible application. + */ + async getEligibleApplications( + locationId: number, + options: { + applicationId: number; + }, + ): Promise; /** * Gets all eligible applications that can be requested for application * offering change. * @param locationId location id. - * @param paginationOptions options to execute the pagination. + * @param options method options: + * - `pagination` options to execute the pagination. * @returns list of eligible applications that can be requested for * application offering change. */ async getEligibleApplications( locationId: number, - paginationOptions: PaginationOptions, - ): Promise> { + options: { + pagination: PaginationOptions; + }, + ): Promise>; + /** + * Gets all eligible applications that can be requested for application + * offering change. + * @param locationId location id. + * @param options method options: + * - `pagination` options to execute the pagination. + * - `applicationId` specific eligible application to be retrieved. + * @returns list of eligible applications that can be requested for + * application offering change. + */ + async getEligibleApplications( + locationId: number, + options: { + pagination?: PaginationOptions; + applicationId?: number; + }, + ): Promise | Application> { const applicationQuery = this.applicationRepo .createQueryBuilder("application") .select([ "application.applicationNumber", "application.id", + "programYear.id", "currentAssessment.id", + "educationProgram.id", + "offering.id", "offering.studyStartDate", "offering.studyEndDate", + "offering.offeringIntensity", "student.id", "user.firstName", "user.lastName", ]) + .innerJoin("application.programYear", "programYear") .innerJoin("application.currentAssessment", "currentAssessment") .innerJoin("currentAssessment.offering", "offering") + .innerJoin("offering.educationProgram", "educationProgram") .leftJoin( "application.applicationOfferingChangeRequest", "applicationOfferingChangeRequest", + "applicationOfferingChangeRequest.applicationOfferingChangeRequestStatus IN (:...status)", + { + status: [ + ApplicationOfferingChangeRequestStatus.InProgressWithSABC, + ApplicationOfferingChangeRequestStatus.InProgressWithStudent, + ], + }, ) .innerJoin("application.student", "student") .innerJoin("student.user", "user") - .where( - new Brackets((qb) => - qb - .where("applicationOfferingChangeRequest.id IS NULL") - .orWhere( - "applicationOfferingChangeRequest.applicationOfferingChangeRequestStatus NOT IN (:...status)", - { - status: [ - ApplicationOfferingChangeRequestStatus.InProgressWithSABC, - ApplicationOfferingChangeRequestStatus.InProgressWithStudent, - ], - }, - ), - ), - ) + .where("applicationOfferingChangeRequest.id IS NULL") .andWhere("application.location.id = :locationId", { locationId }) .andWhere("application.applicationStatus = :applicationStatus", { applicationStatus: ApplicationStatus.Completed, }) .andWhere("application.isArchived = false"); - if (paginationOptions.searchCriteria?.trim()) { + if (options?.applicationId) { + applicationQuery.andWhere("application.id = :applicationId", { + applicationId: options.applicationId, + }); + // If searching by application id there is no need for pagination. + return applicationQuery.getOne(); + } + + if (options.pagination.searchCriteria?.trim()) { applicationQuery .andWhere( new Brackets((qb) => { @@ -84,19 +127,20 @@ export class ApplicationOfferingChangeRequestService { ) .setParameter( "searchCriteria", - `%${paginationOptions.searchCriteria}%`, + `%${options.pagination.searchCriteria}%`, ); } applicationQuery .orderBy( transformToApplicationEntitySortField( - paginationOptions.sortField, - paginationOptions.sortOrder, + options.pagination.sortField, + options.pagination.sortOrder, ), ) - .offset(paginationOptions.page * paginationOptions.pageLimit) - .limit(paginationOptions.pageLimit); + .offset(options.pagination.page * options.pagination.pageLimit) + .limit(options.pagination.pageLimit); + const [result, count] = await applicationQuery.getManyAndCount(); return { results: result, @@ -172,4 +216,114 @@ export class ApplicationOfferingChangeRequestService { count, }; } + + /** + * Get the Application Offering Change Request by its id. + * @param id the Application Offering Change Request id. + * @param options method options: + * - `locationId`: location for authorization. + * @returns application offering change request. + */ + async getById( + id: number, + options?: { + locationId?: number; + }, + ): Promise { + return this.applicationOfferingChangeRequestRepo.findOne({ + select: { + id: true, + reason: true, + applicationOfferingChangeRequestStatus: true, + activeOffering: { + id: true, + }, + requestedOffering: { + id: true, + name: true, + educationProgram: { + id: true, + name: true, + }, + }, + application: { + id: true, + applicationNumber: true, + location: { + id: true, + name: true, + }, + student: { + id: true, + user: { + id: true, + firstName: true, + lastName: true, + }, + }, + }, + assessedNote: { + id: true, + description: true, + }, + }, + relations: { + application: { + location: true, + student: { + user: true, + }, + }, + activeOffering: true, + requestedOffering: { + educationProgram: true, + }, + assessedNote: true, + }, + where: { + id, + application: { + location: { id: options?.locationId }, + }, + }, + }); + } + + /** + * Creates a new application offering change request. + * @param locationId location id used for authorization. + * @param applicationId application that will have the change requested. + * @param offeringId offering being requested to be changed. + * @param reason reason provided by the institution to have the offering changed. + * @param auditUserId used creating the request. + * @returns created application offering change request. + */ + async createRequest( + locationId: number, + applicationId: number, + offeringId: number, + reason: string, + auditUserId: number, + ): Promise { + // Validates if the application is eligible to have an offering request change created. + const application = await this.getEligibleApplications(locationId, { + applicationId, + }); + if (!application) { + throw new Error("Application not found or it is not eligible."); + } + // TODO: Apply the same validations from PIR. + const newRequest = new ApplicationOfferingChangeRequest(); + newRequest.application = application; + newRequest.activeOffering = application.currentAssessment.offering; + newRequest.requestedOffering = { + id: offeringId, + } as EducationProgramOffering; + newRequest.creator = { id: auditUserId } as User; + newRequest.applicationOfferingChangeRequestStatus = + ApplicationOfferingChangeRequestStatus.InProgressWithStudent; + newRequest.reason = reason; + + return this.applicationOfferingChangeRequestRepo.save(newRequest); + } } diff --git a/sources/packages/backend/apps/api/src/services/education-program-offering/education-program-offering.service.ts b/sources/packages/backend/apps/api/src/services/education-program-offering/education-program-offering.service.ts index 3f7b3860f5..c80d0b3282 100644 --- a/sources/packages/backend/apps/api/src/services/education-program-offering/education-program-offering.service.ts +++ b/sources/packages/backend/apps/api/src/services/education-program-offering/education-program-offering.service.ts @@ -656,12 +656,17 @@ export class EducationProgramOfferingService extends RecordDataModelService { const offeringQuery = this.repo .createQueryBuilder("offering") @@ -694,13 +699,19 @@ export class EducationProgramOfferingService extends RecordDataModelService { const subQuery = qb @@ -719,6 +730,12 @@ export class EducationProgramOfferingService extends RecordDataModelService + +

+ {{ offeringViewData.programName }} +

+

+ {{ offeringViewData.programDescription }} +

+ + + + + + + + +
+ +

+ {{ offeringViewData.offeringName }} +

+ + + + + + + + + + + + + + + + + + + +

Study costs

+ + + + + + + + + + + + + + +
+ + + diff --git a/sources/packages/web/src/components/generic/BodyHeader.vue b/sources/packages/web/src/components/generic/BodyHeader.vue index 0ddc70af89..0c38747445 100644 --- a/sources/packages/web/src/components/generic/BodyHeader.vue +++ b/sources/packages/web/src/components/generic/BodyHeader.vue @@ -1,7 +1,9 @@ diff --git a/sources/packages/web/src/components/generic/HeaderTitleValue.vue b/sources/packages/web/src/components/generic/HeaderTitleValue.vue index 7dca50b470..6d59426f16 100644 --- a/sources/packages/web/src/components/generic/HeaderTitleValue.vue +++ b/sources/packages/web/src/components/generic/HeaderTitleValue.vue @@ -3,7 +3,7 @@ Used when we need to display header title and the value --> @@ -18,7 +20,7 @@ export default defineComponent({ props: { propertyValue: { type: String, - required: true, + required: false, }, propertyTitle: { type: String, diff --git a/sources/packages/web/src/components/institutions/request-a-change/AvailableToChangeSummary.vue b/sources/packages/web/src/components/institutions/request-a-change/AvailableToChangeSummary.vue index 1858db08fe..a6ea64bba4 100644 --- a/sources/packages/web/src/components/institutions/request-a-change/AvailableToChangeSummary.vue +++ b/sources/packages/web/src/components/institutions/request-a-change/AvailableToChangeSummary.vue @@ -31,22 +31,25 @@ :items="applications?.results" :items-length="applications?.count" :loading="loading" + item-value="applicationId" v-model:items-per-page="DEFAULT_PAGE_LIMIT" @update:options="paginationAndSortEvent" > - -