Files
emaui/apps/backoffice/src/app/features/license-review/components/ScheduleExamModal.tsx

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