mirror of
https://github.com/Tria-plc/emaui.git
synced 2026-08-26 19:12:50 +00:00
108 lines
3.4 KiB
TypeScript
108 lines
3.4 KiB
TypeScript
import { useState } from 'react';
|
|
import { Alert, Button, Modal, Select, Stack, Text } from '@mantine/core';
|
|
import { IconCalendarEvent } from '@tabler/icons-react';
|
|
import { useTranslation } from 'react-i18next';
|
|
import { ModalFooter } from '@ema-platform/ui';
|
|
import { useGetEligibleExamsQuery } from '@ema-platform/api';
|
|
|
|
interface Props {
|
|
opened: boolean;
|
|
applicationId: string;
|
|
applicantName: string;
|
|
loading: boolean;
|
|
onClose: () => void;
|
|
onConfirm: (payload: { examId: string; examDate?: string }) => void;
|
|
}
|
|
|
|
/**
|
|
* Places a candidate who has paid the examination fee into an existing sitting.
|
|
*
|
|
* Sessions are picked from the exam calendar rather than typed, because the
|
|
* candidate joins a scheduled sitting — this is an assignment, not the creation
|
|
* of a per-candidate appointment. Scoped to sittings whose certification
|
|
* matches this application's rank, so a Chief Mate candidate cannot be seated
|
|
* into an OOW Deck sitting by accident.
|
|
*/
|
|
export function ScheduleExamModal({
|
|
opened,
|
|
applicationId,
|
|
applicantName,
|
|
loading,
|
|
onClose,
|
|
onConfirm,
|
|
}: Props) {
|
|
const { t } = useTranslation();
|
|
const { data: exams, isLoading } = useGetEligibleExamsQuery(applicationId, { skip: !opened });
|
|
const [examId, setExamId] = useState<string | null>(null);
|
|
|
|
const options = (exams ?? []).map((exam) => ({
|
|
value: exam.id,
|
|
label: [exam.title?.en ?? exam.title?.am ?? t('exam.untitled', 'Untitled exam'), exam.date]
|
|
.filter(Boolean)
|
|
.join(' — '),
|
|
}));
|
|
const selected = exams?.find((exam) => exam.id === examId);
|
|
|
|
function confirm() {
|
|
if (!examId) return;
|
|
onConfirm({
|
|
examId,
|
|
examDate: selected?.date ? String(selected.date) : undefined,
|
|
});
|
|
}
|
|
|
|
return (
|
|
<Modal
|
|
opened={opened}
|
|
onClose={onClose}
|
|
title={t('review.actions.scheduleExam', 'Schedule exam')}
|
|
>
|
|
<Stack>
|
|
<Text size="sm" c="dimmed">
|
|
{t('review.scheduleExam.intro', {
|
|
defaultValue:
|
|
'Assign {{applicant}} to a scheduled sitting. They will be notified with the date and their admission number.',
|
|
applicant: applicantName,
|
|
})}
|
|
</Text>
|
|
|
|
{!isLoading && options.length === 0 ? (
|
|
<Alert color="orange" icon={<IconCalendarEvent size={16} />}>
|
|
{t(
|
|
'review.scheduleExam.noSessions',
|
|
'No exam sessions for this rank exist yet. Create one in the Exams area first.',
|
|
)}
|
|
</Alert>
|
|
) : (
|
|
<Select
|
|
label={t('review.scheduleExam.session', 'Exam session')}
|
|
placeholder={t('review.scheduleExam.pick', 'Choose a sitting')}
|
|
data={options}
|
|
value={examId}
|
|
onChange={setExamId}
|
|
disabled={isLoading}
|
|
searchable
|
|
withAsterisk
|
|
/>
|
|
)}
|
|
|
|
<Text size="xs" c="dimmed">
|
|
{t(
|
|
'review.scheduleExam.admissionHint',
|
|
'An admission number is issued automatically when the candidate is seated.',
|
|
)}
|
|
</Text>
|
|
|
|
<ModalFooter>
|
|
<Button variant="default" onClick={onClose}>
|
|
{t('common.cancel', 'Cancel')}
|
|
</Button>
|
|
<Button loading={loading} disabled={!examId} onClick={confirm}>
|
|
{t('review.scheduleExam.confirm', 'Schedule')}
|
|
</Button>
|
|
</ModalFooter>
|
|
</Stack>
|
|
</Modal>
|
|
);
|
|
}
|