mirror of
https://github.com/Tria-plc/emaui.git
synced 2026-09-09 05:58:19 +00:00
fix(exam): reflect authoritative exam state and lock engine-graded results
- COC queue status cell shows the server-derived exam state (registered, present, sat, passed, failed) instead of the lagging application status. - Portal examStageFor prefers the server's examState so portal and back office never disagree; NOT_SITTING stage added. - Exam roster shows each candidate's result and lock; Regrade hidden once a result exists. - Record Result modal: only unmarked candidates, empty score boxes (no silent zeros), reason required, backend refusals translated. - Result page: auto-graded marks read-only, per-applicant publish. - Exam page: session window fields, Add Question menu (bank / Excel / scratch), wait metrics panel; PUBLISHED exam status removed. - Portal: exam window shown, early launch and eligibility refusals explained. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Group, Paper, SimpleGrid, Text, Title, Badge } from '@mantine/core';
|
||||
import { IconHourglass } from '@tabler/icons-react';
|
||||
import { useGetExamWaitMetricsQuery } from '../api/exam-api';
|
||||
import type { WaitStat } from '../types/exam';
|
||||
|
||||
function minutes(value: number | null, t: (key: string, options?: Record<string, unknown>) => string): string {
|
||||
if (value === null) return '—';
|
||||
const abs = Math.abs(value);
|
||||
const label =
|
||||
abs >= 1440
|
||||
? t('exam.metrics.days', { value: Math.round((abs / 1440) * 10) / 10 })
|
||||
: abs >= 60
|
||||
? t('exam.metrics.hours', { value: Math.round((abs / 60) * 10) / 10 })
|
||||
: t('exam.metrics.minutes', { value: Math.round(abs * 10) / 10 });
|
||||
return value < 0 ? `−${label}` : label;
|
||||
}
|
||||
|
||||
function StatCard({ label, stat, t }: { label: string; stat: WaitStat; t: (key: string, options?: Record<string, unknown>) => string }) {
|
||||
return (
|
||||
<Paper withBorder radius="md" p="md">
|
||||
<Text fz="xs" c="dimmed" tt="uppercase" fw={600}>{label}</Text>
|
||||
<Text fz={22} fw={700} mt={4}>{minutes(stat.averageMinutes, t)}</Text>
|
||||
<Text fz="xs" c="dimmed">{t('exam.metrics.average')}</Text>
|
||||
<Group gap="md" mt="xs">
|
||||
<Text fz="xs">{t('exam.metrics.min')}: <b>{minutes(stat.minMinutes, t)}</b></Text>
|
||||
<Text fz="xs">{t('exam.metrics.max')}: <b>{minutes(stat.maxMinutes, t)}</b></Text>
|
||||
<Text fz="xs">{t('exam.metrics.count')}: <b>{stat.count}</b></Text>
|
||||
</Group>
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Exam wait metrics — how long candidates waited at each step up to the
|
||||
* sitting, read off the registration, attendance and attempt timestamps
|
||||
* the workflow already writes. Analytics only: nothing here can change a
|
||||
* registration, an attendance ruling, a result or a certificate.
|
||||
*/
|
||||
export function ExamWaitMetricsPanel({ examId }: { examId: string }) {
|
||||
const { t } = useTranslation();
|
||||
const { data, isError } = useGetExamWaitMetricsQuery(examId);
|
||||
if (isError || !data) return null;
|
||||
|
||||
return (
|
||||
<Paper withBorder radius="lg" p="lg">
|
||||
<Group justify="space-between" mb="xs">
|
||||
<Group gap="xs">
|
||||
<IconHourglass size={18} />
|
||||
<Title order={5}>{t('exam.metrics.section')}</Title>
|
||||
</Group>
|
||||
<Group gap="xs">
|
||||
<Badge variant="light" color="gray">{t('exam.metrics.candidates', { count: data.candidateCount })}</Badge>
|
||||
<Badge variant="light" color="teal">{t('exam.metrics.attended', { count: data.attendedCount })}</Badge>
|
||||
<Badge variant="light" color="blue">{t('exam.metrics.started', { count: data.startedCount })}</Badge>
|
||||
</Group>
|
||||
</Group>
|
||||
<Text fz="xs" c="dimmed" mb="md">{t('exam.metrics.hint')}</Text>
|
||||
<SimpleGrid cols={{ base: 1, sm: 2, lg: 4 }} spacing="md">
|
||||
<StatCard label={t('exam.metrics.registrationToScheduled')} stat={data.registrationToScheduled} t={t} />
|
||||
<StatCard label={t('exam.metrics.scheduledToAttendance')} stat={data.scheduledToAttendance} t={t} />
|
||||
<StatCard label={t('exam.metrics.attendanceToExamStart')} stat={data.attendanceToExamStart} t={t} />
|
||||
<StatCard label={t('exam.metrics.scheduledToExamStart')} stat={data.scheduledToExamStart} t={t} />
|
||||
</SimpleGrid>
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user