mirror of
https://github.com/Tria-plc/emaui.git
synced 2026-08-26 13:02:50 +00:00
ui: update ui look on exam manag
This commit is contained in:
@@ -18,7 +18,7 @@ import {
|
||||
import { IconInfoCircle, IconCheck, IconX } from '@tabler/icons-react';
|
||||
import { notify } from '@ema-platform/ui';
|
||||
import { useApiQuery } from '@ema-platform/api';
|
||||
import { useCreateResultMutation } from '../api/result-api';
|
||||
import { useCreateResultMutation, useUpdateResultMutation } from '../api/result-api';
|
||||
import type { Exam } from '../../exam/types/exam';
|
||||
|
||||
function InfoRow({ label, value }: { label: string; value: string }) {
|
||||
@@ -50,6 +50,7 @@ export function RecordResultModal({
|
||||
params: { q: 'w=type:=:SEAFARER' },
|
||||
});
|
||||
const [createResult, { isLoading: isSaving }] = useCreateResultMutation();
|
||||
const [updateResult] = useUpdateResultMutation();
|
||||
|
||||
const seafarers = profilesRes?.items ?? [];
|
||||
const questions = exam.questions ?? [];
|
||||
@@ -85,13 +86,16 @@ export function RecordResultModal({
|
||||
score: scores[q.id] ?? 0,
|
||||
remark: questionRemarks[q.id] ?? '',
|
||||
}));
|
||||
await createResult({
|
||||
const created = await createResult({
|
||||
seafarerId: selectedSeafarerId,
|
||||
examId: exam.id,
|
||||
resultBreakdowns: breakdowns,
|
||||
totalScore,
|
||||
remark: remark ? { en: remark, am: '' } : undefined,
|
||||
}).unwrap();
|
||||
if (passed && created.id) {
|
||||
await updateResult({ id: created.id, status: 'PASSED' }).unwrap();
|
||||
}
|
||||
notify.success(`Result recorded — ${passed ? 'PASSED' : 'FAILED'} (${totalScore}/${exam.cuttingPoint})`);
|
||||
setSelectedSeafarerId(null);
|
||||
setScores({});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState } from 'react';
|
||||
import { useState, type ElementType } from 'react';
|
||||
import {
|
||||
Stack,
|
||||
Title,
|
||||
@@ -32,6 +32,10 @@ import {
|
||||
IconClock,
|
||||
IconMapPin,
|
||||
IconPlus,
|
||||
IconClipboardList,
|
||||
IconCircleCheck,
|
||||
IconCircleX,
|
||||
IconChartBar,
|
||||
} from '@tabler/icons-react';
|
||||
import { notify } from '@ema-platform/ui';
|
||||
import { useGetResultsQuery, useLazyGetResultQuery, useDeleteResultMutation } from '../api/result-api';
|
||||
@@ -45,6 +49,41 @@ const STATUS_COLOR: Record<string, string> = {
|
||||
FAILED: 'red',
|
||||
};
|
||||
|
||||
function ResultStat({
|
||||
label,
|
||||
value,
|
||||
sub,
|
||||
icon: Icon,
|
||||
color,
|
||||
}: {
|
||||
label: string;
|
||||
value: string;
|
||||
sub?: string;
|
||||
icon: ElementType;
|
||||
color: string;
|
||||
}) {
|
||||
return (
|
||||
<Paper p="lg" radius="lg" withBorder>
|
||||
<Group justify="space-between" align="flex-start">
|
||||
<ThemeIcon size={46} radius="md" variant="light" color={color}>
|
||||
<Icon size={22} />
|
||||
</ThemeIcon>
|
||||
{sub && (
|
||||
<Badge variant="light" color={color} radius="sm">
|
||||
{sub}
|
||||
</Badge>
|
||||
)}
|
||||
</Group>
|
||||
<Text fz={30} fw={800} mt="md" lh={1.1}>
|
||||
{value}
|
||||
</Text>
|
||||
<Text size="sm" c="dimmed" mt={4}>
|
||||
{label}
|
||||
</Text>
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
|
||||
function InfoRow({ label, value }: { label: string; value: string }) {
|
||||
return (
|
||||
<div>
|
||||
@@ -242,6 +281,14 @@ export function ResultPage() {
|
||||
|
||||
const filtered = results.filter((r) => !examFilter || r.examId === examFilter);
|
||||
|
||||
const total = results.length;
|
||||
const passedCount = results.filter((r) => r.status === 'PASSED').length;
|
||||
const failedCount = total - passedCount;
|
||||
const passRate = total ? Math.round((passedCount / total) * 100) : 0;
|
||||
const avgScore = total
|
||||
? (results.reduce((s, r) => s + Number(r.totalScore || 0), 0) / total).toFixed(1)
|
||||
: '0';
|
||||
|
||||
const getExamTitle = (id: string) => exams.find((e) => e.id === id)?.title?.en ?? '-';
|
||||
|
||||
const viewDetail = (result: Result) => {
|
||||
@@ -276,6 +323,13 @@ export function ResultPage() {
|
||||
</Button>
|
||||
</Group>
|
||||
|
||||
<SimpleGrid cols={{ base: 2, lg: 4 }} spacing="lg">
|
||||
<ResultStat label="Total Results" value={String(total)} icon={IconClipboardList} color="blue" />
|
||||
<ResultStat label="Passed" value={String(passedCount)} sub={`${passRate}%`} icon={IconCircleCheck} color="teal" />
|
||||
<ResultStat label="Failed" value={String(failedCount)} sub={`${total ? 100 - passRate : 0}%`} icon={IconCircleX} color="red" />
|
||||
<ResultStat label="Avg Score" value={avgScore} icon={IconChartBar} color="indigo" />
|
||||
</SimpleGrid>
|
||||
|
||||
<Paper withBorder radius="md">
|
||||
<Group p="md" justify="space-between" wrap="wrap" gap="sm">
|
||||
<Text fw={600}>Results</Text>
|
||||
@@ -312,7 +366,20 @@ export function ResultPage() {
|
||||
<Table.Td><Text fz="sm">{r.exam ? r.exam.title.en : getExamTitle(r.examId)}</Text></Table.Td>
|
||||
<Table.Td><Text fz="sm" fw={600}>{r.totalScore}</Text></Table.Td>
|
||||
<Table.Td>
|
||||
<Badge size="sm" variant="light" color={STATUS_COLOR[r.status]}>{r.status}</Badge>
|
||||
<Badge
|
||||
size="sm"
|
||||
variant="light"
|
||||
color={STATUS_COLOR[r.status]}
|
||||
leftSection={
|
||||
<Box
|
||||
w={6}
|
||||
h={6}
|
||||
style={{ borderRadius: 999, background: `var(--mantine-color-${STATUS_COLOR[r.status]}-6)` }}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{r.status}
|
||||
</Badge>
|
||||
</Table.Td>
|
||||
<Table.Td><Text fz="sm">{new Date(r.createdAt).toLocaleDateString()}</Text></Table.Td>
|
||||
<Table.Td>
|
||||
|
||||
7255
exam-result.pen
Normal file
7255
exam-result.pen
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user