From 27b59e459136b4bbab9e54257cb083b50ea691ce Mon Sep 17 00:00:00 2001 From: Luis Hankel Date: Tue, 1 Mar 2022 18:36:25 +0100 Subject: [PATCH 1/3] Refactored admin postponement request views and added edit postponement request modal --- .../components/admin/PostponementsList.vue | 294 ------------------ .../admin/modals/EditPostponementModal.vue | 131 ++++++++ .../PostponementsList.vue | 120 +++++++ .../PostponementsListEntry.vue | 145 +++++++++ .../PostponementsListFilters.vue | 65 ++++ client/src/models/Postponement.ts | 19 +- client/src/utils/gateways.ts | 66 ++-- server/src/controllers/postponementRequest.ts | 28 +- server/src/models/internshipModule.ts | 49 ++- server/src/routes/postponementRequest.ts | 12 + 10 files changed, 600 insertions(+), 329 deletions(-) delete mode 100644 client/src/components/admin/PostponementsList.vue create mode 100644 client/src/components/admin/modals/EditPostponementModal.vue create mode 100644 client/src/components/admin/postponement-requests/PostponementsList.vue create mode 100644 client/src/components/admin/postponement-requests/PostponementsListEntry.vue create mode 100644 client/src/components/admin/postponement-requests/PostponementsListFilters.vue diff --git a/client/src/components/admin/PostponementsList.vue b/client/src/components/admin/PostponementsList.vue deleted file mode 100644 index e94433b5..00000000 --- a/client/src/components/admin/PostponementsList.vue +++ /dev/null @@ -1,294 +0,0 @@ - - - - - diff --git a/client/src/components/admin/modals/EditPostponementModal.vue b/client/src/components/admin/modals/EditPostponementModal.vue new file mode 100644 index 00000000..a9140e7a --- /dev/null +++ b/client/src/components/admin/modals/EditPostponementModal.vue @@ -0,0 +1,131 @@ + + + diff --git a/client/src/components/admin/postponement-requests/PostponementsList.vue b/client/src/components/admin/postponement-requests/PostponementsList.vue new file mode 100644 index 00000000..526d18f5 --- /dev/null +++ b/client/src/components/admin/postponement-requests/PostponementsList.vue @@ -0,0 +1,120 @@ + + + + + diff --git a/client/src/components/admin/postponement-requests/PostponementsListEntry.vue b/client/src/components/admin/postponement-requests/PostponementsListEntry.vue new file mode 100644 index 00000000..a7d47a91 --- /dev/null +++ b/client/src/components/admin/postponement-requests/PostponementsListEntry.vue @@ -0,0 +1,145 @@ + + + + + diff --git a/client/src/components/admin/postponement-requests/PostponementsListFilters.vue b/client/src/components/admin/postponement-requests/PostponementsListFilters.vue new file mode 100644 index 00000000..efb78f5d --- /dev/null +++ b/client/src/components/admin/postponement-requests/PostponementsListFilters.vue @@ -0,0 +1,65 @@ + + + + + diff --git a/client/src/models/Postponement.ts b/client/src/models/Postponement.ts index 874ba1fa..d9be605a 100644 --- a/client/src/models/Postponement.ts +++ b/client/src/models/Postponement.ts @@ -1,5 +1,14 @@ +export interface PostponementRequester { + _id: string; + firstName: string; + lastName: string; + studentProfile: { + studentId: string; + }; +} + export default class Postponement { - id = ''; + _id = ''; newSemester = ''; @@ -7,10 +16,12 @@ export default class Postponement { reason = ''; - user = { - id: '', - studentId: '', + user: PostponementRequester = { + _id: '', firstName: '', lastName: '', + studentProfile: { + studentId: '', + }, } } diff --git a/client/src/utils/gateways.ts b/client/src/utils/gateways.ts index 3401d052..1d4b5644 100644 --- a/client/src/utils/gateways.ts +++ b/client/src/utils/gateways.ts @@ -3,6 +3,7 @@ import { showErrorNotification } from '@/utils/notification'; import Student from '@/models/Student'; import Internship from '@/models/Internship'; import InternshipModule from '@/models/InternshipModule'; +import Postponement from '@/models/Postponement'; import Company from '@/models/Company'; export const getStudentsList = async (semester: string | undefined): Promise => apiClient @@ -161,33 +162,52 @@ export const deleteCompany = async (companyId: string): Promise => { } }; -export const getPostponementsList = async () => apiClient.get('/postponement-requests') - .then((res) => res.data) - .catch((err) => { - console.log(err); +export const getPostponementsList = async (): Promise => { + try { + const response = await apiClient.get('/postponement-requests'); + return response.data; + } catch (err: any) { + if (err.response?.data?.error?.message) err.message = err.response.data.error.message; + await showErrorNotification(`Fehler beim Laden der Verschiebungsanträge [ERROR: ${err.message}]`); return []; - }); + } +}; -export const getPostponement = async (id: string) => apiClient.get(`/postponement-requests/${id}`) - .then((res) => res.data) - .catch((err) => { - console.log(err); - return []; - }); +export const acceptPostponement = async (id: string): Promise => { + try { + await apiClient.patch(`/postponement-requests/${id}/accept`); + return true; + } catch (err: any) { + if (err.response?.data?.error?.message) err.message = err.response.data.error.message; + await showErrorNotification(`Fehler beim Akzeptieren des Verschiebungsantrags [ERROR: ${err.message}]`); + return false; + } +}; -export const acceptPostponement = async (id: string) => apiClient.patch(`/postponement-requests/${id}/accept`) - .then((res) => res) - .catch((err) => { - console.log(err); - return []; - }); +export const rejectPostponement = async (id: string): Promise => { + try { + await apiClient.patch(`/postponement-requests/${id}/reject`); + return true; + } catch (err: any) { + if (err.response?.data?.error?.message) err.message = err.response.data.error.message; + await showErrorNotification(`Fehler beim Ablehnen des Verschiebungsantrags [ERROR: ${err.message}]`); + return false; + } +}; -export const rejectPostponement = async (id: string) => apiClient.patch(`/postponement-requests/${id}/reject`) - .then((res) => res) - .catch((err) => { - console.log(err); - return []; - }); +export const updatePostponement = async ( + id: string, + payload: unknown, +): Promise => { + try { + const response = await apiClient.patch(`/postponement-requests/${id}`, payload); + return response.data; + } catch (err: any) { + if (err.response?.data?.error?.message) err.message = err.response.data.error.message; + await showErrorNotification(`Fehler beim Updaten vom Verschiebungsantrag ${id} [ERROR: ${err.message}]`); + return null; + } +}; // TODO: Also used in Home. Consolidate export const loadAvailableSemesters = async (): Promise => { diff --git a/server/src/controllers/postponementRequest.ts b/server/src/controllers/postponementRequest.ts index 084d5d32..19efd0ab 100644 --- a/server/src/controllers/postponementRequest.ts +++ b/server/src/controllers/postponementRequest.ts @@ -59,7 +59,7 @@ export async function listPostponementRequests(req: Request, res: Response): Pro for (const request of postponementRequests) { request.user = await User.findOne( { "studentProfile.internship": request._id }, - "firstName lastName" + "firstName lastName studentProfile.studentId" ); requestsWithUsers.push(request); } @@ -121,3 +121,29 @@ export function processPostponementRequest(accept: boolean) { res.send(); }; } + +export async function editPostponementRequest( + req: Request, + res: Response, + next: NextFunction +): Promise { + // Get internship module by id + const internshipModule = await InternshipModule.findById(req.params.id); + if (!internshipModule) + return next(new NotFound(`Internship module with id ${req.params.id} not found`)); + // Get admin user id + const user = await User.findOne({ emailAddress: req.user?.email }).lean(); + // Accept or reject postponement + try { + const updatedInternshipModule = await internshipModule.editPostponement(user?._id, req.body); + const updatedPostponementRequest = updatedInternshipModule.getRecentPostponementRequest(); + res.json({ + _id: updatedInternshipModule._id, + newSemester: updatedPostponementRequest.changes?.newSemester, + newSemesterOfStudy: updatedPostponementRequest.changes?.newSemesterOfStudy, + reason: updatedPostponementRequest.comment, + }); + } catch (e) { + return next(new BadRequest(e.message)); + } +} diff --git a/server/src/models/internshipModule.ts b/server/src/models/internshipModule.ts index 494b33af..dd009f27 100644 --- a/server/src/models/internshipModule.ts +++ b/server/src/models/internshipModule.ts @@ -36,6 +36,13 @@ export interface IInternshipModule extends Document { rejectPostponement(creator: Types.ObjectId, reason?: string): Promise; + editPostponement( + creator: Types.ObjectId, + updatedProps: Record + ): Promise; + + getRecentPostponementRequest(): IEvent; + passAep(creator: Types.ObjectId): Promise; submitCompleteDocumentsPdf(creator: Types.ObjectId, newPath: string): Promise; @@ -78,7 +85,7 @@ const InternshipModuleSchema = new Schema({ /*******************/ /* Model Methods */ /*******************/ -InternshipModuleSchema.methods.plan = async function () { +InternshipModuleSchema.methods.plan = async function (): Promise { const defaultSemester = Semester.getUpcoming().toString(); const defaultSemesterOfStudy = 4; this.events.push({ @@ -103,7 +110,7 @@ InternshipModuleSchema.methods.requestPostponement = async function ( newSemester: string, newSemesterOfStudy: number, reason: string -) { +): Promise { const user = await User.findById(creator); if (!user) throw new Error("Creator (User) with that objectId does not exist."); @@ -131,13 +138,11 @@ InternshipModuleSchema.methods.requestPostponement = async function ( InternshipModuleSchema.methods.acceptPostponement = async function ( creator: Types.ObjectId, reason?: string -) { +): Promise { const user = await User.findById(creator); if (!user?.isAdmin) throw new Error("Only Admins may accept a postponement."); - const recentPostponementRequest = this.events - .filter((e) => e.changes?.newSemester) - .reduce((event, next) => ((event.timestamp ?? 0) > (next.timestamp ?? 0) ? event : next)); + const recentPostponementRequest = this.getRecentPostponementRequest(); this.inSemester = recentPostponementRequest.changes?.newSemester as string; this.inSemesterOfStudy = recentPostponementRequest.changes?.newSemesterOfStudy as number; @@ -160,7 +165,7 @@ InternshipModuleSchema.methods.acceptPostponement = async function ( InternshipModuleSchema.methods.rejectPostponement = async function ( creator: Types.ObjectId, reason?: string -) { +): Promise { const user = await User.findById(creator); if (!user?.isAdmin) throw new Error("Only Admins may reject a postponement."); @@ -178,6 +183,36 @@ InternshipModuleSchema.methods.rejectPostponement = async function ( return this.save(); }; +InternshipModuleSchema.methods.editPostponement = async function ( + creator: Types.ObjectId, + updatedProps: Record +): Promise { + const user = await User.findById(creator); + if (!user?.isAdmin) throw new Error("Only Admins may accept a postponement."); + + const recentPostponementRequest = this.getRecentPostponementRequest(); + if (recentPostponementRequest.changes === undefined) recentPostponementRequest.changes = {}; + + const updatableProps = ["newSemester", "newSemesterOfStudy"]; + for (const prop of updatableProps) { + if (updatedProps[prop] !== undefined) { + recentPostponementRequest.changes[prop] = updatedProps[prop]; + } + } + + if (updatedProps.reason !== undefined) + recentPostponementRequest.comment = updatedProps.reason as string; + + this.markModified("events"); + return this.save(); +}; + +InternshipModuleSchema.methods.getRecentPostponementRequest = function (): IEvent { + return this.events + .filter((e) => e.changes?.newSemester) + .reduce((event, next) => ((event.timestamp ?? 0) > (next.timestamp ?? 0) ? event : next)); +}; + InternshipModuleSchema.methods.passAep = async function (creator: Types.ObjectId) { if (this.aepPassed) throw new Error("Aep has already been passed."); diff --git a/server/src/routes/postponementRequest.ts b/server/src/routes/postponementRequest.ts index 1b9ca220..2904711e 100644 --- a/server/src/routes/postponementRequest.ts +++ b/server/src/routes/postponementRequest.ts @@ -3,6 +3,7 @@ import authMiddleware from "../authentication/middleware"; import * as asyncHandler from "express-async-handler"; import { createPostponementRequest, + editPostponementRequest, listPostponementRequests, processPostponementRequest, } from "../controllers/postponementRequest"; @@ -42,4 +43,15 @@ postponementRequestRouter.patch( asyncHandler(processPostponementRequest(false)) ); +postponementRequestRouter.patch( + "/:id", + authMiddleware(true), + param("id").custom(isObjectId), + body("newSemester").custom(Semester.isValidSemesterString).optional(), + body("newSemesterOfStudy").isInt({ gt: 1 }).optional(), + body("reason").isString().optional(), + validate, + asyncHandler(editPostponementRequest) +); + export default postponementRequestRouter; From 3897ff00a06f071ef6fa8e630bab2a1f2a5cee62 Mon Sep 17 00:00:00 2001 From: Luis Hankel Date: Thu, 27 Oct 2022 14:42:44 +0200 Subject: [PATCH 2/3] Replaced semester text input field with dropdown select for editing postponement requests --- .../admin/modals/EditPostponementModal.vue | 49 +++--- .../postponements/CreatePostponement.vue | 141 +++++++++--------- client/src/locales/de/postponement.ts | 5 + client/src/locales/en/postponement.ts | 9 +- client/src/utils/gateways.ts | 14 +- client/src/views/Home.vue | 16 +- 6 files changed, 125 insertions(+), 109 deletions(-) diff --git a/client/src/components/admin/modals/EditPostponementModal.vue b/client/src/components/admin/modals/EditPostponementModal.vue index a9140e7a..34b1afd6 100644 --- a/client/src/components/admin/modals/EditPostponementModal.vue +++ b/client/src/components/admin/modals/EditPostponementModal.vue @@ -9,7 +9,7 @@