mirror of
https://github.com/Tria-plc/emaui.git
synced 2026-09-08 06:35:44 +00:00
feat: enhance exam result details and display comprehensive report information commit
This commit is contained in:
@@ -39,8 +39,9 @@ import {
|
|||||||
IconX,
|
IconX,
|
||||||
} from '@tabler/icons-react';
|
} from '@tabler/icons-react';
|
||||||
import { notify } from '@ema-platform/ui';
|
import { notify } from '@ema-platform/ui';
|
||||||
import { useApiQuery, useApiMutation } from '@ema-platform/api';
|
import { useApiQuery } from '@ema-platform/api';
|
||||||
import { useGetExamQuery, useUpdateExamMutation } from '../api/exam-api';
|
import { useGetExamQuery, useUpdateExamMutation } from '../api/exam-api';
|
||||||
|
import { useCreateResultMutation } from '../../result/api/result-api';
|
||||||
import type { Exam, ExamStatus } from '../types/exam';
|
import type { Exam, ExamStatus } from '../types/exam';
|
||||||
|
|
||||||
const STATUS_COLOR: Record<string, string> = {
|
const STATUS_COLOR: Record<string, string> = {
|
||||||
@@ -80,7 +81,7 @@ function RecordResultModal({
|
|||||||
url: '/profiles',
|
url: '/profiles',
|
||||||
params: { q: 'w=type:=:SEAFARER' },
|
params: { q: 'w=type:=:SEAFARER' },
|
||||||
});
|
});
|
||||||
const [createResult, { isLoading: isSaving }] = useApiMutation();
|
const [createResult, { isLoading: isSaving }] = useCreateResultMutation();
|
||||||
|
|
||||||
const seafarers = profilesRes?.items ?? [];
|
const seafarers = profilesRes?.items ?? [];
|
||||||
const questions = exam.questions ?? [];
|
const questions = exam.questions ?? [];
|
||||||
@@ -113,15 +114,11 @@ function RecordResultModal({
|
|||||||
remark: '',
|
remark: '',
|
||||||
}));
|
}));
|
||||||
await createResult({
|
await createResult({
|
||||||
url: '/results',
|
seafarerId: selectedSeafarerId,
|
||||||
method: 'POST',
|
examId: exam.id,
|
||||||
body: {
|
resultBreakdowns: breakdowns,
|
||||||
seafarerId: selectedSeafarerId,
|
totalScore,
|
||||||
examId: exam.id,
|
remark: remark ? { en: remark, am: '' } : undefined,
|
||||||
resultBreakdowns: breakdowns,
|
|
||||||
totalScore,
|
|
||||||
remark: remark ? { en: remark, am: '' } : undefined,
|
|
||||||
},
|
|
||||||
}).unwrap();
|
}).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);
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ const resultApi = baseApi.injectEndpoints({
|
|||||||
providesTags: ['Api'],
|
providesTags: ['Api'],
|
||||||
}),
|
}),
|
||||||
getResult: builder.query<Result, string>({
|
getResult: builder.query<Result, string>({
|
||||||
query: (id) => `/results/${id}`,
|
query: (id) => `/results/${id}?i=exam,profile`,
|
||||||
providesTags: ['Api'],
|
providesTags: ['Api'],
|
||||||
}),
|
}),
|
||||||
createResult: builder.mutation<Result, CreateResultPayload>({
|
createResult: builder.mutation<Result, CreateResultPayload>({
|
||||||
@@ -39,6 +39,7 @@ const resultApi = baseApi.injectEndpoints({
|
|||||||
export const {
|
export const {
|
||||||
useGetResultsQuery,
|
useGetResultsQuery,
|
||||||
useGetResultQuery,
|
useGetResultQuery,
|
||||||
|
useLazyGetResultQuery,
|
||||||
useCreateResultMutation,
|
useCreateResultMutation,
|
||||||
useUpdateResultMutation,
|
useUpdateResultMutation,
|
||||||
useDeleteResultMutation,
|
useDeleteResultMutation,
|
||||||
|
|||||||
@@ -15,10 +15,25 @@ import {
|
|||||||
SimpleGrid,
|
SimpleGrid,
|
||||||
Divider,
|
Divider,
|
||||||
Button,
|
Button,
|
||||||
|
ThemeIcon,
|
||||||
|
Box,
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import { useDisclosure } from '@mantine/hooks';
|
import { useDisclosure } from '@mantine/hooks';
|
||||||
import { IconInfoCircle, IconEye } from '@tabler/icons-react';
|
import {
|
||||||
import { useGetResultsQuery } from '../api/result-api';
|
IconInfoCircle,
|
||||||
|
IconEye,
|
||||||
|
IconTrash,
|
||||||
|
IconUser,
|
||||||
|
IconCertificate,
|
||||||
|
IconScoreboard,
|
||||||
|
IconCheck,
|
||||||
|
IconX,
|
||||||
|
IconCalendar,
|
||||||
|
IconClock,
|
||||||
|
IconMapPin,
|
||||||
|
} from '@tabler/icons-react';
|
||||||
|
import { notify } from '@ema-platform/ui';
|
||||||
|
import { useGetResultsQuery, useLazyGetResultQuery, useDeleteResultMutation } from '../api/result-api';
|
||||||
import { useGetExamsQuery } from '../../exam/api/exam-api';
|
import { useGetExamsQuery } from '../../exam/api/exam-api';
|
||||||
import type { Result } from '../types/result';
|
import type { Result } from '../types/result';
|
||||||
|
|
||||||
@@ -27,66 +42,158 @@ const STATUS_COLOR: Record<string, string> = {
|
|||||||
FAILED: 'red',
|
FAILED: 'red',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function InfoRow({ label, value }: { label: string; value: string }) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Text fz="xs" c="dimmed" tt="uppercase" fw={600}>{label}</Text>
|
||||||
|
<Text fz="sm" fw={500}>{value || '—'}</Text>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function ResultDetail({ result }: { result: Result }) {
|
function ResultDetail({ result }: { result: Result }) {
|
||||||
|
const exam = result.exam;
|
||||||
|
const profile = result.profile;
|
||||||
|
const totalScore = Number(result.totalScore);
|
||||||
|
const cuttingPoint = exam?.cuttingPoint ? Number(exam.cuttingPoint) : 0;
|
||||||
|
const passed = totalScore >= cuttingPoint;
|
||||||
|
|
||||||
|
const formatTime = (t: { days?: number; hours?: number; minutes?: number } | null | undefined) => {
|
||||||
|
if (!t) return '—';
|
||||||
|
const parts: string[] = [];
|
||||||
|
if (t.days) parts.push(`${t.days}d`);
|
||||||
|
if (t.hours) parts.push(`${t.hours}h`);
|
||||||
|
if (t.minutes) parts.push(`${t.minutes}m`);
|
||||||
|
return parts.join(' ') || '—';
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatDate = (d: string | undefined) => d ? new Date(d).toLocaleDateString() : '—';
|
||||||
|
const formatDob = (d: string | undefined) => d ? new Date(d).toLocaleDateString() : '—';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack gap="md">
|
<Stack gap="md">
|
||||||
<SimpleGrid cols={2} spacing="sm">
|
{/* Seafarer Profile */}
|
||||||
<div>
|
<Paper withBorder p="md" radius="md">
|
||||||
<Text fz="xs" c="dimmed" tt="uppercase" fw={600}>Seafarer</Text>
|
<Group gap="sm" mb="sm">
|
||||||
<Text fz="sm" fw={500}>
|
<ThemeIcon size="sm" variant="light" color="blue" radius="xl">
|
||||||
{result.seafarer
|
<IconUser size={14} />
|
||||||
? `${result.seafarer.firstName} ${result.seafarer.middleName ?? ''} ${result.seafarer.lastName}`
|
</ThemeIcon>
|
||||||
: result.seafarerId}
|
<Text fw={600} fz="sm">Seafarer Profile</Text>
|
||||||
</Text>
|
</Group>
|
||||||
</div>
|
<SimpleGrid cols={{ base: 2, sm: 4 }} spacing="sm">
|
||||||
<div>
|
<InfoRow label="Full Name" value={profile ? `${profile.firstName} ${profile.middleName ?? ''} ${profile.lastName}` : result.seafarerId} />
|
||||||
<Text fz="xs" c="dimmed" tt="uppercase" fw={600}>Exam</Text>
|
<InfoRow label="Gender" value={profile?.gender ?? '—'} />
|
||||||
<Text fz="sm">{result.exam?.title?.en ?? result.examId}</Text>
|
<InfoRow label="Date of Birth" value={formatDob(profile?.dob)} />
|
||||||
</div>
|
<InfoRow label="Marital Status" value={profile?.maritalStatus ?? '—'} />
|
||||||
<div>
|
</SimpleGrid>
|
||||||
<Text fz="xs" c="dimmed" tt="uppercase" fw={600}>Total Score</Text>
|
</Paper>
|
||||||
<Text fz="sm" fw={700}>{result.totalScore}</Text>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Text fz="xs" c="dimmed" tt="uppercase" fw={600}>Status</Text>
|
|
||||||
<Badge size="sm" variant="light" color={STATUS_COLOR[result.status]}>{result.status}</Badge>
|
|
||||||
</div>
|
|
||||||
</SimpleGrid>
|
|
||||||
|
|
||||||
<Divider />
|
{/* Exam Details */}
|
||||||
|
{exam && (
|
||||||
<Text fw={600} fz="sm">Score Breakdown</Text>
|
<Paper withBorder p="md" radius="md">
|
||||||
{result.resultBreakdowns.length === 0 ? (
|
<Group gap="sm" mb="sm">
|
||||||
<Text fz="sm" c="dimmed">No breakdown data</Text>
|
<ThemeIcon size="sm" variant="light" color="violet" radius="xl">
|
||||||
) : (
|
<IconCertificate size={14} />
|
||||||
<Table striped>
|
</ThemeIcon>
|
||||||
<Table.Thead>
|
<Text fw={600} fz="sm">Exam Details</Text>
|
||||||
<Table.Tr>
|
</Group>
|
||||||
<Table.Th>Question ID</Table.Th>
|
<SimpleGrid cols={{ base: 2, sm: 4 }} spacing="sm">
|
||||||
<Table.Th>Score</Table.Th>
|
<InfoRow label="Exam Title" value={exam.title?.en ?? '—'} />
|
||||||
<Table.Th>Remark</Table.Th>
|
<InfoRow label="Title (Amharic)" value={exam.title?.am ?? '—'} />
|
||||||
</Table.Tr>
|
<InfoRow label="Type" value={exam.type ?? '—'} />
|
||||||
</Table.Thead>
|
<InfoRow label="Form" value={exam.form ?? '—'} />
|
||||||
<Table.Tbody>
|
<InfoRow label="Venue" value={exam.venue ?? '—'} />
|
||||||
{result.resultBreakdowns.map((b, i) => (
|
<InfoRow label="Date" value={formatDate(exam.date)} />
|
||||||
<Table.Tr key={i}>
|
<InfoRow label="Pass Mark" value={String(cuttingPoint)} />
|
||||||
<Table.Td><Text fz="xs">{b.questionId.slice(0, 8)}</Text></Table.Td>
|
<InfoRow label="Evaluation" value={exam.evaluationMethod ?? '—'} />
|
||||||
<Table.Td><Text fz="sm" fw={600}>{b.score}</Text></Table.Td>
|
<InfoRow label="Selection" value={exam.selectionMethod ?? '—'} />
|
||||||
<Table.Td><Text fz="sm">{b.remark ?? '—'}</Text></Table.Td>
|
<InfoRow label="Time Allowed" value={formatTime(exam.givenTime)} />
|
||||||
</Table.Tr>
|
<InfoRow label="Status" value={exam.status ?? '—'} />
|
||||||
))}
|
<InfoRow label="Questions" value={String(exam.questions?.length ?? 0)} />
|
||||||
</Table.Tbody>
|
</SimpleGrid>
|
||||||
</Table>
|
{exam.direction?.en && (
|
||||||
|
<>
|
||||||
|
<Divider my="sm" />
|
||||||
|
<InfoRow label="Directions" value={exam.direction.en} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Paper>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Score Breakdown */}
|
||||||
|
<Paper withBorder p="md" radius="md">
|
||||||
|
<Group gap="sm" mb="sm">
|
||||||
|
<ThemeIcon size="sm" variant="light" color="orange" radius="xl">
|
||||||
|
<IconScoreboard size={14} />
|
||||||
|
</ThemeIcon>
|
||||||
|
<Text fw={600} fz="sm">Score Breakdown</Text>
|
||||||
|
</Group>
|
||||||
|
{result.resultBreakdowns.length === 0 ? (
|
||||||
|
<Text fz="sm" c="dimmed">No breakdown data</Text>
|
||||||
|
) : (
|
||||||
|
<Table striped>
|
||||||
|
<Table.Thead>
|
||||||
|
<Table.Tr>
|
||||||
|
<Table.Th>#</Table.Th>
|
||||||
|
<Table.Th>Question</Table.Th>
|
||||||
|
<Table.Th>Form</Table.Th>
|
||||||
|
<Table.Th>Max</Table.Th>
|
||||||
|
<Table.Th>Score</Table.Th>
|
||||||
|
<Table.Th>Remark</Table.Th>
|
||||||
|
</Table.Tr>
|
||||||
|
</Table.Thead>
|
||||||
|
<Table.Tbody>
|
||||||
|
{result.resultBreakdowns.map((b, i) => {
|
||||||
|
const q = exam?.questions?.find((eq) => eq.id === b.questionId);
|
||||||
|
return (
|
||||||
|
<Table.Tr key={b.questionId}>
|
||||||
|
<Table.Td><Text fz="xs">{i + 1}</Text></Table.Td>
|
||||||
|
<Table.Td>
|
||||||
|
<Text fz="xs" lineClamp={2} maw={220}>
|
||||||
|
{q?.title?.en ?? b.questionId.slice(0, 8)}
|
||||||
|
</Text>
|
||||||
|
</Table.Td>
|
||||||
|
<Table.Td>
|
||||||
|
{q?.form && (
|
||||||
|
<Badge size="xs" variant="light" color={q.form === 'ESSAY' ? 'blue' : 'violet'}>
|
||||||
|
{q.form}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</Table.Td>
|
||||||
|
<Table.Td><Text fz="sm" fw={600}>{q?.points ?? '—'}</Text></Table.Td>
|
||||||
|
<Table.Td><Text fz="sm" fw={600}>{b.score}</Text></Table.Td>
|
||||||
|
<Table.Td><Text fz="xs">{b.remark || '—'}</Text></Table.Td>
|
||||||
|
</Table.Tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Table.Tbody>
|
||||||
|
</Table>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Divider my="sm" />
|
||||||
|
|
||||||
|
<Paper withBorder p="sm" radius="md" bg="gray.0">
|
||||||
|
<SimpleGrid cols={3} spacing="sm">
|
||||||
|
<InfoRow label="Total Score" value={`${totalScore} / ${cuttingPoint}`} />
|
||||||
|
<InfoRow label="Pass Mark" value={String(cuttingPoint)} />
|
||||||
|
<div>
|
||||||
|
<Text fz="xs" c="dimmed" tt="uppercase" fw={600}>Status</Text>
|
||||||
|
<Group gap={4} mt={2}>
|
||||||
|
{passed ? (
|
||||||
|
<><IconCheck size={14} color="var(--mantine-color-teal-6)" /><Text fz="sm" fw={700} c="teal">PASSED</Text></>
|
||||||
|
) : (
|
||||||
|
<><IconX size={14} color="var(--mantine-color-red-6)" /><Text fz="sm" fw={700} c="red">FAILED</Text></>
|
||||||
|
)}
|
||||||
|
</Group>
|
||||||
|
</div>
|
||||||
|
</SimpleGrid>
|
||||||
|
</Paper>
|
||||||
|
</Paper>
|
||||||
|
|
||||||
{result.remark && (
|
{result.remark && (
|
||||||
<>
|
<Paper withBorder p="md" radius="md">
|
||||||
<Divider />
|
<InfoRow label="Officer Remark" value={result.remark.en || result.remark.am || '—'} />
|
||||||
<div>
|
</Paper>
|
||||||
<Text fz="xs" c="dimmed" tt="uppercase" fw={600}>Officer Remark</Text>
|
|
||||||
<Text fz="sm">{result.remark.en || result.remark.am}</Text>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
@@ -95,13 +202,17 @@ function ResultDetail({ result }: { result: Result }) {
|
|||||||
export function ResultPage() {
|
export function ResultPage() {
|
||||||
const { data: examRes } = useGetExamsQuery();
|
const { data: examRes } = useGetExamsQuery();
|
||||||
const { data, isLoading, isError } = useGetResultsQuery();
|
const { data, isLoading, isError } = useGetResultsQuery();
|
||||||
|
const [fetchDetail, { data: detailResult, isFetching: isDetailLoading }] = useLazyGetResultQuery();
|
||||||
|
|
||||||
const exams = examRes?.items ?? [];
|
const exams = examRes?.items ?? [];
|
||||||
const results = data?.items ?? [];
|
const results = data?.items ?? [];
|
||||||
|
|
||||||
|
const [deleteResult] = useDeleteResultMutation();
|
||||||
|
|
||||||
const [examFilter, setExamFilter] = useState<string | null>(null);
|
const [examFilter, setExamFilter] = useState<string | null>(null);
|
||||||
const [selectedResult, setSelectedResult] = useState<Result | null>(null);
|
|
||||||
const [detailOpened, { open: openDetail, close: closeDetail }] = useDisclosure(false);
|
const [detailOpened, { open: openDetail, close: closeDetail }] = useDisclosure(false);
|
||||||
|
const [deleteTarget, setDeleteTarget] = useState<Result | null>(null);
|
||||||
|
const [deleteOpened, { open: openDelete, close: closeDelete }] = useDisclosure(false);
|
||||||
|
|
||||||
const examOptions = exams.map((e) => ({ value: e.id, label: `${e.title.en} (${e.date})` }));
|
const examOptions = exams.map((e) => ({ value: e.id, label: `${e.title.en} (${e.date})` }));
|
||||||
|
|
||||||
@@ -110,10 +221,22 @@ export function ResultPage() {
|
|||||||
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) => {
|
||||||
setSelectedResult(result);
|
fetchDetail(result.id);
|
||||||
openDetail();
|
openDetail();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleDelete = async () => {
|
||||||
|
if (!deleteTarget) return;
|
||||||
|
try {
|
||||||
|
await deleteResult(deleteTarget.id).unwrap();
|
||||||
|
notify.success('Result deleted');
|
||||||
|
closeDelete();
|
||||||
|
setDeleteTarget(null);
|
||||||
|
} catch {
|
||||||
|
notify.error('Failed to delete result');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (isLoading) return <Center py="xl"><Loader /></Center>;
|
if (isLoading) return <Center py="xl"><Loader /></Center>;
|
||||||
if (isError) return <Alert icon={<IconInfoCircle size={16} />} color="red" title="Error loading results" />;
|
if (isError) return <Alert icon={<IconInfoCircle size={16} />} color="red" title="Error loading results" />;
|
||||||
|
|
||||||
@@ -164,14 +287,25 @@ export function ResultPage() {
|
|||||||
</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>
|
||||||
<Button
|
<Group gap="xs">
|
||||||
size="xs"
|
<Button
|
||||||
variant="subtle"
|
size="xs"
|
||||||
leftSection={<IconEye size={13} />}
|
variant="subtle"
|
||||||
onClick={() => viewDetail(r)}
|
leftSection={<IconEye size={13} />}
|
||||||
>
|
onClick={() => viewDetail(r)}
|
||||||
View
|
>
|
||||||
</Button>
|
View
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="xs"
|
||||||
|
variant="subtle"
|
||||||
|
color="red"
|
||||||
|
leftSection={<IconTrash size={13} />}
|
||||||
|
onClick={() => { setDeleteTarget(r); openDelete(); }}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
</Table.Td>
|
</Table.Td>
|
||||||
</Table.Tr>
|
</Table.Tr>
|
||||||
))}
|
))}
|
||||||
@@ -186,8 +320,28 @@ export function ResultPage() {
|
|||||||
</Table>
|
</Table>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|
||||||
<Modal opened={detailOpened} onClose={closeDetail} title="Result Detail" size="lg" radius="lg">
|
<Modal
|
||||||
{selectedResult && <ResultDetail result={selectedResult} />}
|
opened={detailOpened}
|
||||||
|
onClose={closeDetail}
|
||||||
|
title="Result Detail"
|
||||||
|
size="xl"
|
||||||
|
radius="lg"
|
||||||
|
>
|
||||||
|
{isDetailLoading ? (
|
||||||
|
<Center py="xl"><Loader /></Center>
|
||||||
|
) : detailResult ? (
|
||||||
|
<ResultDetail result={detailResult} />
|
||||||
|
) : (
|
||||||
|
<Text c="dimmed" ta="center" py="xl">No data available</Text>
|
||||||
|
)}
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
<Modal opened={deleteOpened} onClose={closeDelete} title="Delete Result" size="sm">
|
||||||
|
<Text mb="md">Are you sure you want to delete this result?</Text>
|
||||||
|
<Group justify="flex-end">
|
||||||
|
<Button variant="default" onClick={closeDelete} size="sm">Cancel</Button>
|
||||||
|
<Button color="red" onClick={handleDelete} size="sm">Delete</Button>
|
||||||
|
</Group>
|
||||||
</Modal>
|
</Modal>
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,3 +1,8 @@
|
|||||||
|
import type { LocalePair } from '../../certification/types/certification';
|
||||||
|
import type { EstimatedTime } from '../../question/types/question';
|
||||||
|
import type { QuestionForm } from '../../question/types/question';
|
||||||
|
export type { QuestionForm };
|
||||||
|
|
||||||
export type ExamResultStatus = 'PASSED' | 'FAILED';
|
export type ExamResultStatus = 'PASSED' | 'FAILED';
|
||||||
|
|
||||||
export interface ResultBreakdown {
|
export interface ResultBreakdown {
|
||||||
@@ -6,12 +11,43 @@ export interface ResultBreakdown {
|
|||||||
remark?: string;
|
remark?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface Profile {
|
||||||
|
id: string;
|
||||||
|
firstName: string;
|
||||||
|
middleName?: string;
|
||||||
|
lastName: string;
|
||||||
|
gender?: string;
|
||||||
|
dob?: string;
|
||||||
|
maritalStatus?: string;
|
||||||
|
type?: string;
|
||||||
|
isComplete?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FullExam {
|
||||||
|
id: string;
|
||||||
|
certificationId?: string;
|
||||||
|
title: LocalePair;
|
||||||
|
direction?: LocalePair | null;
|
||||||
|
date?: string;
|
||||||
|
givenTime?: EstimatedTime | null;
|
||||||
|
type?: string;
|
||||||
|
form?: QuestionForm;
|
||||||
|
venue?: string;
|
||||||
|
administrationMethod?: string;
|
||||||
|
evaluationMethod?: string;
|
||||||
|
selectionMethod?: string;
|
||||||
|
cuttingPoint?: number;
|
||||||
|
status?: string;
|
||||||
|
questions?: { id: string; title: LocalePair; form: QuestionForm; points: number }[];
|
||||||
|
}
|
||||||
|
|
||||||
export interface Result {
|
export interface Result {
|
||||||
id: string;
|
id: string;
|
||||||
seafarerId: string;
|
seafarerId: string;
|
||||||
seafarer?: { id: string; firstName: string; middleName?: string; lastName: string };
|
seafarer?: { id: string; firstName: string; middleName?: string; lastName: string };
|
||||||
examId: string;
|
examId: string;
|
||||||
exam?: { id: string; title: { en: string; am: string } };
|
exam?: FullExam;
|
||||||
|
profile?: Profile;
|
||||||
resultBreakdowns: ResultBreakdown[];
|
resultBreakdowns: ResultBreakdown[];
|
||||||
totalScore: number;
|
totalScore: number;
|
||||||
remark: { en: string; am: string } | null;
|
remark: { en: string; am: string } | null;
|
||||||
|
|||||||
Reference in New Issue
Block a user