diff --git a/components/dialog/AdminScoringDialog.tsx b/components/dialog/AdminScoringDialog.tsx index 90f4ad3..bf625bb 100644 --- a/components/dialog/AdminScoringDialog.tsx +++ b/components/dialog/AdminScoringDialog.tsx @@ -7,6 +7,7 @@ import TmuaGradeBox from '@/components/dialog/TmuaGradeBox' import Dropdown from '@/components/general/Dropdown' import LabelText from '@/components/general/LabelText' import { adminAccess } from '@/lib/access' +import { dateFormatting } from '@/lib/constants' import { upsertAdminScoring } from '@/lib/query/forms' import { FormPassbackState } from '@/lib/types' import { decimalToNumber } from '@/lib/utils' @@ -41,8 +42,8 @@ const AdminScoringForm: FC = ({ data, readOnly }) => { {internalReview?.lastAdminEditOn && internalReview?.lastAdminEditBy && ( - Last edited by {internalReview.lastAdminEditBy} on{' '} - {format(internalReview.lastAdminEditOn, "dd/MM/yy 'at' HH:mm")} + Last admin scoring by {internalReview.lastAdminEditBy} on{' '} + {format(internalReview.lastAdminEditOn, dateFormatting)} )} diff --git a/components/dialog/ReviewerScoringDialog.tsx b/components/dialog/ReviewerScoringDialog.tsx index dd8428a..e0199e2 100644 --- a/components/dialog/ReviewerScoringDialog.tsx +++ b/components/dialog/ReviewerScoringDialog.tsx @@ -5,6 +5,7 @@ import FormWrapper from '@/components/dialog/FormWrapper' import GenericDialog from '@/components/dialog/GenericDialog' import LabelText from '@/components/general/LabelText' import { reviewerAccess } from '@/lib/access' +import { dateFormatting } from '@/lib/constants' import { upsertReviewerScoring } from '@/lib/query/forms' import { FormPassbackState } from '@/lib/types' import { ord } from '@/lib/utils' @@ -45,7 +46,7 @@ const ReviewerScoringForm: FC = ({ data, readOnly }) = {internalReview?.lastReviewerEditOn && ( - Last edited on {format(internalReview.lastReviewerEditOn, "dd/MM/yy 'at' HH:mm")} + Last reviewed on {format(internalReview.lastReviewerEditOn, dateFormatting)} )} @@ -142,7 +143,7 @@ const ReviewerScoringDialog: FC = ({ data, userEmail const handleFormSuccess = () => setIsOpen(false) const upsertReviewerScoringWithId = (prevState: FormPassbackState, formData: FormData) => - upsertReviewerScoring(data.id, prevState, formData) + upsertReviewerScoring(data.id, userEmail, prevState, formData) const readOnly = !reviewerAccess(data.reviewer?.login, userEmail) diff --git a/components/dialog/UgTutorDialog.tsx b/components/dialog/UgTutorDialog.tsx index 55a3784..47b152c 100644 --- a/components/dialog/UgTutorDialog.tsx +++ b/components/dialog/UgTutorDialog.tsx @@ -9,6 +9,7 @@ import Dropdown from '@/components/general/Dropdown' import LabelText from '@/components/general/LabelText' import { ApplicationRow } from '@/components/table/ApplicationTable' import { adminAccess } from '@/lib/access' +import { dateFormatting } from '@/lib/constants' import { insertComment, updateOutcomes } from '@/lib/query/forms' import { FormPassbackState } from '@/lib/types' import { decimalToNumber } from '@/lib/utils' @@ -32,6 +33,7 @@ import { TextArea, TextField } from '@radix-ui/themes' +import { format } from 'date-fns' import React, { FC, useEffect, useMemo, useState } from 'react' type Tab = 'outcomes' | 'comments' @@ -86,6 +88,13 @@ const UgTutorForm: FC = ({ return ( + {internalReview?.lastUserEditOn && internalReview?.lastUserEditBy && ( + + Last overall edit by {internalReview.lastUserEditBy} on{' '} + {format(internalReview.lastUserEditOn, dateFormatting)} + + )} + = ({ data, reviewerLogin, user }) => const upsertOutcomeWithId = async (prevState: FormPassbackState, formData: FormData) => { return await updateOutcomes( id, + email, data.outcomes.map((o) => ({ id: o.id, degreeCode: o.degreeCode diff --git a/components/table/ApplicationTable.tsx b/components/table/ApplicationTable.tsx index 5918dd8..4653584 100644 --- a/components/table/ApplicationTable.tsx +++ b/components/table/ApplicationTable.tsx @@ -140,7 +140,11 @@ const ApplicationTable: FC = ({ }), columnHelper.accessor('nextAction', { cell: (info) => ( - + ), header: 'Next Action', id: SEARCH_PARAM_NEXT_ACTION @@ -258,9 +262,10 @@ const WPColourMap: Record = { [WP.NOT_CALCULATED]: 'yellow' } -const NextActionCell: FC<{ nextAction: NextAction; applicationId: number }> = ({ +const NextActionCell: FC<{ nextAction: NextAction; applicationId: number; userEmail: string }> = ({ nextAction, - applicationId + applicationId, + userEmail }) => { return ( @@ -271,6 +276,7 @@ const NextActionCell: FC<{ nextAction: NextAction; applicationId: number }> = ({ color="grass" onClick={async () => { await updateNextAction( + userEmail, nextAction === NextAction.INFORM_CANDIDATE ? NextAction.FINAL_CHECK : NextAction.CANDIDATE_INFORMED, diff --git a/lib/constants.ts b/lib/constants.ts new file mode 100644 index 0000000..b1ac257 --- /dev/null +++ b/lib/constants.ts @@ -0,0 +1 @@ +export const dateFormatting = "dd/MM/yy 'at' HH:mm" diff --git a/lib/csv/upload.ts b/lib/csv/upload.ts index 3245a37..3c0b390 100644 --- a/lib/csv/upload.ts +++ b/lib/csv/upload.ts @@ -192,6 +192,7 @@ function updateAdminScoring( if (currentNextAction === NextAction.ADMIN_SCORING_WITH_TMUA) nextNextAction = NextAction.REVIEWER_SCORING + const currentTimestamp = new Date() return prisma.application.update({ where: { admissionsCycle_cid: { @@ -211,7 +212,9 @@ function updateAdminScoring( extracurricularAdminScore: a.extracurricularAdminScore, examComments: a.examComments, lastAdminEditBy: userEmail, - lastAdminEditOn: new Date() + lastAdminEditOn: currentTimestamp, + lastUserEditBy: userEmail, + lastUserEditOn: currentTimestamp } } } diff --git a/lib/query/forms.ts b/lib/query/forms.ts index ff02b13..94b5daa 100644 --- a/lib/query/forms.ts +++ b/lib/query/forms.ts @@ -50,6 +50,7 @@ export async function upsertAdminScoring( } }) + const currentTimestamp = new Date() await prisma.internalReview.upsert({ where: { applicationId @@ -60,14 +61,18 @@ export async function upsertAdminScoring( extracurricularAdminScore, examComments, lastAdminEditBy: adminLogin, - lastAdminEditOn: new Date() + lastAdminEditOn: currentTimestamp, + lastUserEditBy: adminLogin, + lastUserEditOn: currentTimestamp }, update: { motivationAdminScore, extracurricularAdminScore, examComments, lastAdminEditBy: adminLogin, - lastAdminEditOn: new Date() + lastAdminEditOn: currentTimestamp, + lastUserEditBy: adminLogin, + lastUserEditOn: currentTimestamp } }) @@ -77,6 +82,7 @@ export async function upsertAdminScoring( export async function upsertReviewerScoring( applicationId: number, + userEmail: string, _: FormPassbackState, formData: FormData ): Promise { @@ -97,6 +103,7 @@ export async function upsertReviewerScoring( } }) + const currentTimestamp = new Date() await prisma.internalReview.update({ where: { applicationId }, data: { @@ -104,7 +111,9 @@ export async function upsertReviewerScoring( extracurricularReviewerScore, referenceReviewerScore, academicComments, - lastReviewerEditOn: new Date() + lastReviewerEditOn: currentTimestamp, + lastUserEditBy: userEmail, + lastUserEditOn: currentTimestamp } }) @@ -114,6 +123,7 @@ export async function upsertReviewerScoring( export async function updateOutcomes( applicationId: number, + userEmail: string, partialOutcomes: { id: number; degreeCode: string }[], _: FormPassbackState, formData: FormData @@ -126,7 +136,7 @@ export async function updateOutcomes( return { id, ...parsedOutcome } }) - await updateNextAction(formData.get('nextAction'), applicationId) + await updateNextAction(userEmail, formData.get('nextAction'), applicationId) for (const { id, offerCode, offerText, decision } of fullOutcomes) { await prisma.outcome.update({ @@ -156,7 +166,7 @@ export async function insertComment( const result = formCommentSchema.safeParse(Object.fromEntries(formData)) if (!result.success) return { status: 'error', message: result.error.issues[0].message } - await updateNextAction(formData.get('nextAction'), applicationId) + await updateNextAction(authorEmail, formData.get('nextAction'), applicationId) await prisma.comment.create({ data: { @@ -171,12 +181,21 @@ export async function insertComment( } export async function updateNextAction( + userEmail: string, nextActionInput: FormDataEntryValue | string | null, applicationId: number ) { - if (!nextActionInput || nextActionInput === 'Unchanged') return + await prisma.internalReview.update({ + where: { applicationId }, + data: { + lastUserEditBy: userEmail, + lastUserEditOn: new Date() + } + }) + if (!nextActionInput || nextActionInput === 'Unchanged') return const nextAction = nextActionField.parse(nextActionInput) + await prisma.application.update({ where: { id: applicationId }, data: { diff --git a/prisma/migrations/20250120134919_add_attributes_to_track_last_edit_on_application/migration.sql b/prisma/migrations/20250120134919_add_attributes_to_track_last_edit_on_application/migration.sql new file mode 100644 index 0000000..c002224 --- /dev/null +++ b/prisma/migrations/20250120134919_add_attributes_to_track_last_edit_on_application/migration.sql @@ -0,0 +1,3 @@ +-- AlterTable +ALTER TABLE "InternalReview" ADD COLUMN "lastUserEditBy" TEXT, +ADD COLUMN "lastUserEditOn" TIMESTAMP(3); diff --git a/prisma/schema/application.prisma b/prisma/schema/application.prisma index e3821f5..1670fd5 100644 --- a/prisma/schema/application.prisma +++ b/prisma/schema/application.prisma @@ -78,6 +78,8 @@ model InternalReview { examComments String? lastAdminEditBy String? lastAdminEditOn DateTime? + lastUserEditBy String? + lastUserEditOn DateTime? motivationReviewerScore Decimal? @db.Decimal(3, 1) extracurricularReviewerScore Decimal? @db.Decimal(3, 1) referenceReviewerScore Decimal? @db.Decimal(3, 1)