import { useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Alert, Badge, Button, Group, Modal, Paper, Select, Stack, Table, Text, Textarea, Title, } from '@mantine/core'; import { IconInfoCircle, IconUserCheck } from '@tabler/icons-react'; import { notify } from '@ema-platform/ui'; import { extractErrorMessage } from '@ema-platform/api'; import { useGetExamRegistrationsQuery, useRecordAttendanceMutation, } from '../api/exam-api'; import type { AttendanceStatus, ExamRegistration } from '../types/exam'; const ATTENDANCE_COLOR: Record = { REGISTERED: 'gray', PRESENT: 'teal', LATE: 'yellow', ABSENT: 'red', WITHDRAWN: 'orange', DISQUALIFIED: 'red', }; /** Rulings that end the sitting, and so must be explained (US-EXAM-009). */ const NEEDS_REMARK: AttendanceStatus[] = ['WITHDRAWN', 'DISQUALIFIED']; const OPTIONS: AttendanceStatus[] = [ 'PRESENT', 'LATE', 'ABSENT', 'WITHDRAWN', 'DISQUALIFIED', ]; /** * The invigilator's register for one session (US-EXAM-009). * * Marking a paper later depends on what is recorded here: an absent, * withdrawn or disqualified candidate has no result to enter. */ export function ExamCandidatesPanel({ examId }: { examId: string }) { const { t } = useTranslation(); const { data: registrations, isError } = useGetExamRegistrationsQuery(examId); const [recordAttendance, { isLoading }] = useRecordAttendanceMutation(); const [target, setTarget] = useState(null); const [status, setStatus] = useState('PRESENT'); const [remark, setRemark] = useState(''); const candidateName = (registration: ExamRegistration) => [ registration.profile?.firstName, registration.profile?.middleName, registration.profile?.lastName, ] .filter(Boolean) .join(' ') || registration.profileId.slice(0, 8); const save = async () => { if (!target) return; if (NEEDS_REMARK.includes(status) && !remark.trim()) { notify.error(t('exam.candidates.remarkRequired')); return; } try { await recordAttendance({ registrationId: target.id, status, remark: remark.trim() || undefined, }).unwrap(); notify.success(t('exam.candidates.recorded')); setTarget(null); setRemark(''); } catch (error) { notify.error(extractErrorMessage(error, t('exam.incidents.error'))); } }; // Officers without the invigilation permission simply do not see the // register; the endpoint refuses them and there is nothing to show. if (isError) return null; return ( {t('exam.candidates.section')} {(registrations ?? []).length === 0 ? ( }> {t('exam.candidates.none')} ) : ( {t('exam.candidates.admission')} {t('exam.candidates.name')} {t('exam.candidates.attempt')} {t('exam.candidates.attendance')} {t('exam.candidates.remark')} {(registrations ?? []).map((registration) => ( {registration.admissionNumber} {candidateName(registration)} {registration.kind === 'RETAKE' ? t('exam.candidates.retake', { n: registration.attemptNumber }) : t('exam.candidates.firstSitting')} {t(`exam.attendance.${registration.attendanceStatus}`)} {registration.attendanceRemark ?? '—'} ))}
)} setTarget(null)} title={t('exam.candidates.attendance')} size="md" radius="lg" > {target ? candidateName(target) : ''} · {target?.admissionNumber}