From 16542eb6d93aae230d2b6a0e193261bc5386e654 Mon Sep 17 00:00:00 2001 From: maximilianilzhoefer Date: Sat, 25 Oct 2025 22:24:17 +0200 Subject: [PATCH 01/12] feat: add back to inReview functionality for Expense Reports and Travel, including API endpoints and UI updates --- backend/controller/expenseReportController.ts | 35 +++++++++++++++++++ backend/controller/travelController.ts | 18 ++++++++++ backend/tests/api/expenseReport.ts | 24 +++++++++++++ backend/tests/api/travel.ts | 14 ++++++++ common/locales/de.json | 11 ++++++ .../expenseReport/ExpenseReportPage.vue | 29 +++++++++++++-- frontend/src/components/travel/TravelPage.vue | 31 +++++++++++++--- 7 files changed, 155 insertions(+), 7 deletions(-) diff --git a/backend/controller/expenseReportController.ts b/backend/controller/expenseReportController.ts index e13d9e3b6..7c03dd505 100644 --- a/backend/controller/expenseReportController.ts +++ b/backend/controller/expenseReportController.ts @@ -292,6 +292,41 @@ export class ExpenseReportExamineController extends Controller { }) } + @Post('inReview') + public async postBackInReview( + @Body() + requestBody: { + project?: IdDocument + _id?: string + name?: string + advances?: IdDocument[] + category?: IdDocument + owner?: IdDocument + comment?: string + }, + @Request() request: AuthenticatedExpressRequest + ) { + const extendedBody = Object.assign(requestBody, { state: ExpenseReportState.IN_REVIEW, editor: request.user._id }) + if (!extendedBody._id) { + if (!extendedBody.name) { + const date = new Date() + extendedBody.name = `${i18n.t('labels.expenses', { lng: request.user.settings.language })} ${i18n.t(`monthsShort.${date.getUTCMonth()}`, { lng: request.user.settings.language })} ${date.getUTCFullYear()}` + } + } + return await this.setter(ExpenseReport, { + requestBody: extendedBody, + cb: (e: IExpenseReport) => sendNotification(e, extendedBody._id ? 'BACK_TO_IN_REVIEW' : undefined), + allowNew: true, + async checkOldObject(oldObject: ExpenseReportDoc) { + if (oldObject.state === ExpenseReportState.REVIEW_COMPLETED && checkIfUserIsProjectSupervisor(request.user, oldObject.project._id)) { + await oldObject.saveToHistory() + return true + } + return false + } + }) + } + @Post('inWork') public async postBackInWork( @Body() diff --git a/backend/controller/travelController.ts b/backend/controller/travelController.ts index 0d6342d49..0b0aa85dc 100644 --- a/backend/controller/travelController.ts +++ b/backend/controller/travelController.ts @@ -525,6 +525,24 @@ export class TravelExamineController extends Controller { }) } + @Post('inReview') + public async postinReview(@Body() requestBody: { _id: string; comment?: string }, @Request() request: AuthenticatedExpressRequest) { + const extendedBody = Object.assign(requestBody, { state: TravelState.IN_REVIEW, editor: request.user._id }) + + return await this.setter(Travel, { + requestBody: extendedBody, + allowNew: false, + cb: (e: ITravel) => sendNotification(e, 'BACK_TO_IN_REVIEW'), + async checkOldObject(oldObject: TravelDoc) { + if ((oldObject.state === TravelState.IN_REVIEW || oldObject.state === TravelState.REVIEW_COMPLETED) && checkIfUserIsProjectSupervisor(request.user, oldObject.project._id)) { + await oldObject.saveToHistory() + return true + } + return false + } + }) + } + @Post('approved') public async postAnyApproved(@Body() requestBody: { _id: string; comment?: string }, @Request() request: AuthenticatedExpressRequest) { const extendedBody = Object.assign(requestBody, { state: TravelState.APPROVED, editor: request.user._id }) diff --git a/backend/tests/api/expenseReport.ts b/backend/tests/api/expenseReport.ts index 6095e5c54..77cf6d235 100644 --- a/backend/tests/api/expenseReport.ts +++ b/backend/tests/api/expenseReport.ts @@ -30,6 +30,16 @@ test.serial('GET /category', async (t) => { } }) +test.serial('POST /expenseReport/inReview', async (t) => { + const res = await agent.post('/expenseReport/inReview').send(expenseReport) + expenseReport = res.body.result + if (res.status === 200) { + t.pass() + } else { + console.log(res.body) + } +}) + test.serial('POST /expenseReport/inWork', async (t) => { const res = await agent.post('/expenseReport/inWork').send(expenseReport) expenseReport = res.body.result @@ -117,6 +127,20 @@ test.serial('POST /expenseReport/underExamination', async (t) => { t.like((res.body.result as ExpenseReport).comments[0], { text: comment, toState: ExpenseReportState.IN_REVIEW }) }) +test.serial('POST /expenseReport/inReview AGAIN', async (t) => { + t.plan(4) + const comment = '' + const res = await agent.post('/expenseReport/inReview').send({ _id: expenseReport._id, comment }) + if (res.status === 200) { + t.pass() + } else { + console.log(res.body) + } + t.is((res.body.result as ExpenseReport).state, ExpenseReportState.IN_REVIEW) + t.is((res.body.result as ExpenseReport).history.length, 2) + t.is((res.body.result as ExpenseReport).comments.length, 1) +}) + test.serial('POST /expenseReport/inWork AGAIN', async (t) => { t.plan(4) const comment = '' diff --git a/backend/tests/api/travel.ts b/backend/tests/api/travel.ts index f2326dae0..d69488c36 100644 --- a/backend/tests/api/travel.ts +++ b/backend/tests/api/travel.ts @@ -250,6 +250,20 @@ test.serial('POST /travel/underExamination AGAIN', async (t) => { t.is((res.body.result as Travel).history.length, 4) }) +test.serial('POST /travel/inReview AGAIN', async (t) => { + t.plan(4) + const comment = '' + const res = await agent.post('/travel/inReview').send({ _id: travel._id, comment }) + if (res.status === 200) { + t.pass() + } else { + console.log(res.body) + } + t.is((res.body.result as Travel).state, TravelState.IN_REVIEW) + t.is((res.body.result as Travel).history.length, 2) + t.is((res.body.result as Travel).comments.length, 1) +}) + // EXAMINE test.serial('POST /examine/travel/reviewCompleted', async (t) => { diff --git a/common/locales/de.json b/common/locales/de.json index 17d5e0b78..fffc00521 100755 --- a/common/locales/de.json +++ b/common/locales/de.json @@ -202,6 +202,7 @@ "auth": "Authentifizierung", "autoSelectAvailableAdvances": "Passende Vorschüsse automatisch auswählen", "backToApplicant": "Zurück zum Antragsteller*in", + "backToReviewer": "Zurück zum Prüfer*in", "balance": "Saldo", "bankDetails": "Bankverbindung", "black": "Schwarz", @@ -513,6 +514,11 @@ "paragraph": "Deine Auslagenabrechnung wurde dir zum Überarbeiten zurückgeschickt.", "subject": "🔙 Auslagen überarbeiten" }, + "BACK_TO_IN_REVIEW": { + "lastParagraph": "{commentator} schrieb: '{comment}'", + "paragraph": "Eine Auslagenabrechnung wurde dir zum prüfen zurückgeschickt.", + "subject": "🔙 Auslagen (erneut) prüfen" + }, "BOOKED_getsDeletedSoon": { "paragraph": "Deine erstattete Auslagenabrechnung '{reportName}' wird in {days} Tagen automatisch gelöscht. Du kannst dir den Bericht noch bis dahin herunterladen.", "subject": "Automatische Löschung steht bevor" @@ -618,6 +624,11 @@ "paragraph": "{owner} hat die Reiseabrechnung fertiggestellt.", "subject": "🛂 Reise zu prüfen" }, + "BACK_TO_IN_REVIEW": { + "lastParagraph": "{commentator} schrieb: '{comment}'", + "paragraph": "Eine Reise wurde dir zum prüfen zurückgeschickt.", + "subject": "🔙 Reise (erneut) prüfen" + }, "REJECTED": { "lastParagraph": "{commentator} schrieb: '{comment}'", "paragraph": "Dein Reiseantrag wurde abgelehnt.", diff --git a/frontend/src/components/expenseReport/ExpenseReportPage.vue b/frontend/src/components/expenseReport/ExpenseReportPage.vue index a0b5b6ada..c94c56e0e 100644 --- a/frontend/src/components/expenseReport/ExpenseReportPage.vue +++ b/frontend/src/components/expenseReport/ExpenseReportPage.vue @@ -215,7 +215,7 @@ + >
@@ -224,7 +224,7 @@ v-model="expenseReport.bookingRemark" :disabled="isReadOnly && !(endpointPrefix === 'examine/' && expenseReport.state === State.IN_REVIEW)">
-
+
-
+
+ >
@@ -287,15 +287,24 @@ {{ t('labels.completeReview') }}
-
+
+
+ +
@@ -496,6 +505,20 @@ async function toExamination() { } } +async function backToinReview() { + const result = await API.setter>(`${props.endpointPrefix}travel/inReview`, { + _id: travel.value._id, + comment: travel.value.comment + }) + if (result.ok) { + if (props.endpointPrefix === 'examine/') { + router.push({ path: '/examine/travel' }) + } else { + setTravel(result.ok) + } + } +} + async function backToApproved() { const result = await API.setter>(`${props.endpointPrefix}travel/approved`, { _id: travel.value._id, From b1c7125791bb131216df29a759d832b23bfdcf04 Mon Sep 17 00:00:00 2001 From: maximilianilzhoefer Date: Sun, 26 Oct 2025 00:05:38 +0200 Subject: [PATCH 02/12] fix: adjust conditional rendering for buttons in ExpenseReportPage to improve UI behavior --- .../src/components/expenseReport/ExpenseReportPage.vue | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/frontend/src/components/expenseReport/ExpenseReportPage.vue b/frontend/src/components/expenseReport/ExpenseReportPage.vue index c94c56e0e..ea1284e27 100644 --- a/frontend/src/components/expenseReport/ExpenseReportPage.vue +++ b/frontend/src/components/expenseReport/ExpenseReportPage.vue @@ -224,7 +224,7 @@ v-model="expenseReport.bookingRemark" :disabled="isReadOnly && !(endpointPrefix === 'examine/' && expenseReport.state === State.IN_REVIEW)">
-
+
-
+
-
+
-
+
-
+
diff --git a/frontend/src/components/travel/TravelPage.vue b/frontend/src/components/travel/TravelPage.vue index 32b5e4d10..bf165e3a3 100644 --- a/frontend/src/components/travel/TravelPage.vue +++ b/frontend/src/components/travel/TravelPage.vue @@ -296,7 +296,7 @@ {{ t(endpointPrefix === 'examine/' ? 'labels.backToApplicant' : 'labels.editAgain') }}
-
+