feat(exam): regrade action + stop inviting a re-take on a finished exam

Two fixes surfaced by actual candidate-flow testing:

- Portal 'My registrations' always showed 'Take exam' regardless of whether
  the candidate had already finished — clicking it just hit
  attempt_already_submitted server-side. The registrations/mine response
  now carries the attempt's status; the row shows a Completed/Time expired
  badge once finished, or 'Resume exam' while still in progress, and only
  invites a fresh start when there's genuinely nothing yet.
- Backoffice candidates panel: a Regrade button next to a SUBMITTED/EXPIRED
  sitting, calling the new POST /exam-attempts/:id/regrade — the officer's
  path to the same staff-triggered regrade, no SQL needed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
mihretue
2026-08-17 12:00:45 +00:00
parent e97e12d979
commit 83d309248d
8 changed files with 116 additions and 19 deletions

View File

@@ -11,6 +11,7 @@ import type {
ExamIncident,
CreateIncidentPayload,
ResolveIncidentPayload,
RegradeOutcome,
} from '../types/exam';
const examApi = baseApi.injectEndpoints({
@@ -92,6 +93,14 @@ const examApi = baseApi.injectEndpoints({
}),
invalidatesTags: ['Api'],
}),
/** Staff-triggered re-run of auto-grading for one finalized attempt. */
regradeAttempt: builder.mutation<RegradeOutcome, string>({
query: (attemptId) => ({
url: `/exam-attempts/${attemptId}/regrade`,
method: 'POST',
}),
invalidatesTags: ['Api'],
}),
}),
overrideExisting: false,
});
@@ -109,4 +118,5 @@ export const {
useGetExamIncidentsQuery,
useRecordIncidentMutation,
useResolveIncidentMutation,
useRegradeAttemptMutation,
} = examApi;