Files
emaui/apps/backoffice/src/app/features/exam/components/ExamWaitMetricsPanel.tsx
mihretu 24fb51de23 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>
2026-09-07 08:12:12 +00:00

68 lines
3.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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>
);
}