mirror of
https://github.com/Tria-plc/emaui.git
synced 2026-08-26 19:12: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 { IconInfoCircle, IconCheck, IconX } from '@tabler/icons-react';
|
||||||
import { notify } from '@ema-platform/ui';
|
import { notify } from '@ema-platform/ui';
|
||||||
import { useApiQuery } from '@ema-platform/api';
|
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';
|
import type { Exam } from '../../exam/types/exam';
|
||||||
|
|
||||||
function InfoRow({ label, value }: { label: string; value: string }) {
|
function InfoRow({ label, value }: { label: string; value: string }) {
|
||||||
@@ -50,6 +50,7 @@ export function RecordResultModal({
|
|||||||
params: { q: 'w=type:=:SEAFARER' },
|
params: { q: 'w=type:=:SEAFARER' },
|
||||||
});
|
});
|
||||||
const [createResult, { isLoading: isSaving }] = useCreateResultMutation();
|
const [createResult, { isLoading: isSaving }] = useCreateResultMutation();
|
||||||
|
const [updateResult] = useUpdateResultMutation();
|
||||||
|
|
||||||
const seafarers = profilesRes?.items ?? [];
|
const seafarers = profilesRes?.items ?? [];
|
||||||
const questions = exam.questions ?? [];
|
const questions = exam.questions ?? [];
|
||||||
@@ -85,13 +86,16 @@ export function RecordResultModal({
|
|||||||
score: scores[q.id] ?? 0,
|
score: scores[q.id] ?? 0,
|
||||||
remark: questionRemarks[q.id] ?? '',
|
remark: questionRemarks[q.id] ?? '',
|
||||||
}));
|
}));
|
||||||
await createResult({
|
const created = await createResult({
|
||||||
seafarerId: selectedSeafarerId,
|
seafarerId: selectedSeafarerId,
|
||||||
examId: exam.id,
|
examId: exam.id,
|
||||||
resultBreakdowns: breakdowns,
|
resultBreakdowns: breakdowns,
|
||||||
totalScore,
|
totalScore,
|
||||||
remark: remark ? { en: remark, am: '' } : undefined,
|
remark: remark ? { en: remark, am: '' } : undefined,
|
||||||
}).unwrap();
|
}).unwrap();
|
||||||
|
if (passed && created.id) {
|
||||||
|
await updateResult({ id: created.id, status: 'PASSED' }).unwrap();
|
||||||
|
}
|
||||||
notify.success(`Result recorded — ${passed ? 'PASSED' : 'FAILED'} (${totalScore}/${exam.cuttingPoint})`);
|
notify.success(`Result recorded — ${passed ? 'PASSED' : 'FAILED'} (${totalScore}/${exam.cuttingPoint})`);
|
||||||
setSelectedSeafarerId(null);
|
setSelectedSeafarerId(null);
|
||||||
setScores({});
|
setScores({});
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useState } from 'react';
|
import { useState, type ElementType } from 'react';
|
||||||
import {
|
import {
|
||||||
Stack,
|
Stack,
|
||||||
Title,
|
Title,
|
||||||
@@ -32,6 +32,10 @@ import {
|
|||||||
IconClock,
|
IconClock,
|
||||||
IconMapPin,
|
IconMapPin,
|
||||||
IconPlus,
|
IconPlus,
|
||||||
|
IconClipboardList,
|
||||||
|
IconCircleCheck,
|
||||||
|
IconCircleX,
|
||||||
|
IconChartBar,
|
||||||
} from '@tabler/icons-react';
|
} from '@tabler/icons-react';
|
||||||
import { notify } from '@ema-platform/ui';
|
import { notify } from '@ema-platform/ui';
|
||||||
import { useGetResultsQuery, useLazyGetResultQuery, useDeleteResultMutation } from '../api/result-api';
|
import { useGetResultsQuery, useLazyGetResultQuery, useDeleteResultMutation } from '../api/result-api';
|
||||||
@@ -45,6 +49,41 @@ const STATUS_COLOR: Record<string, string> = {
|
|||||||
FAILED: 'red',
|
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 }) {
|
function InfoRow({ label, value }: { label: string; value: string }) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@@ -242,6 +281,14 @@ export function ResultPage() {
|
|||||||
|
|
||||||
const filtered = results.filter((r) => !examFilter || r.examId === examFilter);
|
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 getExamTitle = (id: string) => exams.find((e) => e.id === id)?.title?.en ?? '-';
|
||||||
|
|
||||||
const viewDetail = (result: Result) => {
|
const viewDetail = (result: Result) => {
|
||||||
@@ -276,6 +323,13 @@ export function ResultPage() {
|
|||||||
</Button>
|
</Button>
|
||||||
</Group>
|
</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">
|
<Paper withBorder radius="md">
|
||||||
<Group p="md" justify="space-between" wrap="wrap" gap="sm">
|
<Group p="md" justify="space-between" wrap="wrap" gap="sm">
|
||||||
<Text fw={600}>Results</Text>
|
<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">{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><Text fz="sm" fw={600}>{r.totalScore}</Text></Table.Td>
|
||||||
<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>
|
||||||
<Table.Td><Text fz="sm">{new Date(r.createdAt).toLocaleDateString()}</Text></Table.Td>
|
<Table.Td><Text fz="sm">{new Date(r.createdAt).toLocaleDateString()}</Text></Table.Td>
|
||||||
<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