mirror of
https://github.com/Tria-plc/emaui.git
synced 2026-09-09 05:58:19 +00:00
- 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>
68 lines
3.2 KiB
TypeScript
68 lines
3.2 KiB
TypeScript
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>
|
||
);
|
||
}
|