mirror of
https://github.com/Tria-plc/emaui.git
synced 2026-08-30 01:48:13 +00:00
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>
127 lines
3.8 KiB
TypeScript
127 lines
3.8 KiB
TypeScript
import { Badge, Button, Text } from '@mantine/core';
|
|
import { IconRefresh, IconUserCheck } from '@tabler/icons-react';
|
|
import type { TFunction } from 'i18next';
|
|
import type { AdvancedColumn } from '@ema-platform/ui';
|
|
import { LICENSE_PERMISSIONS, RequirePermission } from '@ema-platform/auth';
|
|
import type { AttendanceStatus, ExamRegistration } from '../../types/exam';
|
|
|
|
const ATTENDANCE_COLOR: Record<AttendanceStatus, string> = {
|
|
REGISTERED: 'gray',
|
|
PRESENT: 'teal',
|
|
LATE: 'yellow',
|
|
ABSENT: 'red',
|
|
WITHDRAWN: 'orange',
|
|
DISQUALIFIED: 'red',
|
|
};
|
|
|
|
export const candidateName = (registration: ExamRegistration) =>
|
|
[
|
|
registration.profile?.firstName,
|
|
registration.profile?.middleName,
|
|
registration.profile?.lastName,
|
|
]
|
|
.filter(Boolean)
|
|
.join(' ') || registration.profileId.slice(0, 8);
|
|
|
|
export function examCandidateColumns(
|
|
t: TFunction,
|
|
handlers: {
|
|
onRecord: (registration: ExamRegistration) => void;
|
|
onRegrade: (registration: ExamRegistration) => void;
|
|
regrading?: string | null;
|
|
},
|
|
): AdvancedColumn<ExamRegistration>[] {
|
|
return [
|
|
{
|
|
header: t('exam.candidates.admission'),
|
|
cell: ({ row }) => (
|
|
<Text fz="sm" ff="monospace" fw={600}>
|
|
{row.original.admissionNumber}
|
|
</Text>
|
|
),
|
|
},
|
|
{
|
|
header: t('exam.candidates.name'),
|
|
cell: ({ row }) => <Text fz="sm">{candidateName(row.original)}</Text>,
|
|
},
|
|
{
|
|
header: t('exam.candidates.attempt'),
|
|
cell: ({ row }) => (
|
|
<Badge
|
|
size="sm"
|
|
variant="light"
|
|
color={row.original.kind === 'RETAKE' ? 'orange' : 'blue'}
|
|
>
|
|
{row.original.kind === 'RETAKE'
|
|
? t('exam.candidates.retake', { n: row.original.attemptNumber })
|
|
: t('exam.candidates.firstSitting')}
|
|
</Badge>
|
|
),
|
|
},
|
|
{
|
|
header: t('exam.candidates.attendance'),
|
|
cell: ({ row }) => (
|
|
<Badge
|
|
size="sm"
|
|
variant="light"
|
|
color={ATTENDANCE_COLOR[row.original.attendanceStatus] ?? 'gray'}
|
|
>
|
|
{t(`exam.attendance.${row.original.attendanceStatus}`)}
|
|
</Badge>
|
|
),
|
|
},
|
|
{
|
|
header: t('exam.candidates.remark'),
|
|
cell: ({ row }) => (
|
|
<Text fz="xs" c="dimmed" maw={220} lineClamp={2}>
|
|
{row.original.attendanceRemark ?? '—'}
|
|
</Text>
|
|
),
|
|
},
|
|
{
|
|
header: '',
|
|
label: t('exam.candidates.record'),
|
|
align: 'right',
|
|
cell: ({ row }) => {
|
|
const attemptStatus = row.original.attempt?.status;
|
|
const canRegrade = attemptStatus === 'SUBMITTED' || attemptStatus === 'EXPIRED';
|
|
return (
|
|
<>
|
|
<RequirePermission
|
|
anyOf={[LICENSE_PERMISSIONS.RECORD_EXAM_ATTENDANCE]}
|
|
hideOnly
|
|
>
|
|
<Button
|
|
size="compact-xs"
|
|
variant="light"
|
|
leftSection={<IconUserCheck size={12} />}
|
|
onClick={() => handlers.onRecord(row.original)}
|
|
mr={canRegrade ? 6 : 0}
|
|
>
|
|
{t('exam.candidates.record')}
|
|
</Button>
|
|
</RequirePermission>
|
|
{canRegrade && (
|
|
<RequirePermission
|
|
anyOf={[LICENSE_PERMISSIONS.RECORD_EXAM_RESULT]}
|
|
hideOnly
|
|
>
|
|
<Button
|
|
size="compact-xs"
|
|
variant="light"
|
|
color="grape"
|
|
leftSection={<IconRefresh size={12} />}
|
|
loading={handlers.regrading === row.original.attempt?.id}
|
|
onClick={() => handlers.onRegrade(row.original)}
|
|
>
|
|
{t('exam.candidates.regrade')}
|
|
</Button>
|
|
</RequirePermission>
|
|
)}
|
|
</>
|
|
);
|
|
},
|
|
},
|
|
];
|
|
}
|