mirror of
https://github.com/Tria-plc/emaui.git
synced 2026-09-07 14:15:45 +00:00
Add rankKey support to certification forms and exams
- Introduced rankKey field in Certification interface and payloads. - Updated CertificationForm to handle rankKey input. - Enhanced ScheduleExamModal to filter exams based on rank. - Added getEligibleExams API query to fetch exams relevant to the application's rank. - Updated related components and types to accommodate new rankKey functionality.
This commit is contained in:
@@ -16,6 +16,15 @@ export function certificationColumns(
|
||||
header: t('certification.columns.description'),
|
||||
cell: ({ row }) => <Text fz="sm" lineClamp={2} maw={250}>{row.original.description[locale]}</Text>,
|
||||
},
|
||||
{
|
||||
header: t('certification.columns.rank', 'Rank'),
|
||||
cell: ({ row }) =>
|
||||
row.original.rankKey ? (
|
||||
<Badge size="sm" variant="outline" color="violet">{row.original.rankKey}</Badge>
|
||||
) : (
|
||||
<Text fz="sm" c="dimmed">—</Text>
|
||||
),
|
||||
},
|
||||
{
|
||||
header: t('certification.columns.status'),
|
||||
cell: ({ row }) => (
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState } from 'react';
|
||||
import {Stack, Button, Modal, Text, TextInput, Textarea, Card} from '@mantine/core';
|
||||
import {Stack, Button, Modal, Text, TextInput, Textarea, Select, Card} from '@mantine/core';
|
||||
import { useDisclosure } from '@mantine/hooks';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {IconPlus} from '@tabler/icons-react';
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
useUpdateCertificationMutation,
|
||||
useDeleteCertificationMutation,
|
||||
} from '../../api/certification-api';
|
||||
import type { Certification } from '../../types/certification';
|
||||
import { RANK_KEY_OPTIONS, type Certification } from '../../types/certification';
|
||||
import { certificationColumns } from './columns';
|
||||
import { certificationActionsColumn } from './actions';
|
||||
|
||||
@@ -22,7 +22,7 @@ function CertificationForm({
|
||||
}: {
|
||||
editing: Certification | null;
|
||||
isSubmitting: boolean;
|
||||
onSubmit: (values: { nameEn: string; nameAm: string; descEn: string; descAm: string }, isEdit: boolean) => void;
|
||||
onSubmit: (values: { nameEn: string; nameAm: string; descEn: string; descAm: string; rankKey: string | null }, isEdit: boolean) => void;
|
||||
onCancel: () => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
@@ -30,6 +30,7 @@ function CertificationForm({
|
||||
const [nameAm, setNameAm] = useState(editing?.name?.am ?? '');
|
||||
const [descEn, setDescEn] = useState(editing?.description?.en ?? '');
|
||||
const [descAm, setDescAm] = useState(editing?.description?.am ?? '');
|
||||
const [rankKey, setRankKey] = useState<string | null>(editing?.rankKey ?? null);
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
@@ -37,7 +38,7 @@ function CertificationForm({
|
||||
notify.error('Name fields are required');
|
||||
return;
|
||||
}
|
||||
onSubmit({ nameEn, nameAm, descEn, descAm }, !!editing);
|
||||
onSubmit({ nameEn, nameAm, descEn, descAm, rankKey }, !!editing);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -48,6 +49,17 @@ function CertificationForm({
|
||||
<TextInput label={t('certification.form.nameAm')} placeholder={t('certification.form.nameAmPlaceholder')} value={nameAm} onChange={(e) => setNameAm(e.currentTarget.value)} size="sm" required />
|
||||
<Textarea label={t('certification.form.descEn')} placeholder={t('certification.form.descEnPlaceholder')} value={descEn} onChange={(e) => setDescEn(e.currentTarget.value)} size="sm" autosize minRows={2} />
|
||||
<Textarea label={t('certification.form.descAm')} placeholder={t('certification.form.descAmPlaceholder')} value={descAm} onChange={(e) => setDescAm(e.currentTarget.value)} size="sm" autosize minRows={2} />
|
||||
<Select
|
||||
label={t('certification.form.rankKey', 'STCW rank (for exam scheduling)')}
|
||||
description={t('certification.form.rankKeyHint', 'Leave blank if this certification is not part of the examined CoC/CoP ladder.')}
|
||||
placeholder={t('certification.form.rankKeyPlaceholder', 'Not rank-specific')}
|
||||
data={RANK_KEY_OPTIONS as unknown as { value: string; label: string }[]}
|
||||
value={rankKey}
|
||||
onChange={setRankKey}
|
||||
size="sm"
|
||||
clearable
|
||||
searchable
|
||||
/>
|
||||
<ModalFooter>
|
||||
<Button variant="default" onClick={onCancel} size="sm">{t('certification.cancel')}</Button>
|
||||
<Button type="submit" size="sm" loading={isSubmitting}>{editing ? t('certification.update') : t('certification.create')}</Button>
|
||||
@@ -80,15 +92,17 @@ export function CertificationPage() {
|
||||
setShowForm(false);
|
||||
};
|
||||
|
||||
const handleSubmit = async (values: { nameEn: string; nameAm: string; descEn: string; descAm: string }, isEdit: boolean) => {
|
||||
const handleSubmit = async (values: { nameEn: string; nameAm: string; descEn: string; descAm: string; rankKey: string | null }, isEdit: boolean) => {
|
||||
const name = { en: values.nameEn, am: values.nameAm };
|
||||
const description = { en: values.descEn, am: values.descAm };
|
||||
try {
|
||||
if (isEdit && editing) {
|
||||
await updateCert({ id: editing.id, name, description }).unwrap();
|
||||
// null clears a previously-set rank; undefined would leave it
|
||||
// untouched server-side, so the two are not interchangeable here.
|
||||
await updateCert({ id: editing.id, name, description, rankKey: values.rankKey }).unwrap();
|
||||
notify.success(t('certification.updated'));
|
||||
} else {
|
||||
await createCert({ name, description }).unwrap();
|
||||
await createCert({ name, description, rankKey: values.rankKey ?? undefined }).unwrap();
|
||||
notify.success(t('certification.created'));
|
||||
}
|
||||
resetForm();
|
||||
|
||||
@@ -3,11 +3,30 @@ export interface LocalePair {
|
||||
am: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* STCW rank an exam certification is for — the join that lets the
|
||||
* schedule-exam picker offer only sittings valid for an application's rank.
|
||||
* Matches `LicenseApplication.formData.certificate.rank` / `rankEngine` /
|
||||
* `proficiency` on the backend. Not every certification is on the examined
|
||||
* ladder, so this stays a plain optional string rather than a required enum.
|
||||
*/
|
||||
export const RANK_KEY_OPTIONS = [
|
||||
{ value: 'OOW_DECK', label: 'Officer of the Watch (Deck)' },
|
||||
{ value: 'CHIEF_MATE', label: 'Chief Mate' },
|
||||
{ value: 'MASTER', label: 'Master' },
|
||||
{ value: 'OOW_ENGINE', label: 'Officer of the Watch (Engine)' },
|
||||
{ value: 'SECOND_ENGINEER', label: 'Second Engineer' },
|
||||
{ value: 'CHIEF_ENGINEER', label: 'Chief Engineer' },
|
||||
{ value: 'ABLE_SEAFARER_DECK', label: 'Able Seafarer Deck' },
|
||||
{ value: 'ABLE_SEAFARER_ENGINE', label: 'Able Seafarer Engine' },
|
||||
] as const;
|
||||
|
||||
export interface Certification {
|
||||
id: string;
|
||||
name: LocalePair;
|
||||
description: LocalePair;
|
||||
isActive: boolean;
|
||||
rankKey: string | null;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
@@ -20,6 +39,7 @@ export interface ListResponse<T> {
|
||||
export interface CreateCertificationPayload {
|
||||
name: LocalePair;
|
||||
description: LocalePair;
|
||||
rankKey?: string;
|
||||
}
|
||||
|
||||
export interface UpdateCertificationPayload {
|
||||
@@ -27,4 +47,6 @@ export interface UpdateCertificationPayload {
|
||||
name?: LocalePair;
|
||||
description?: LocalePair;
|
||||
isActive?: boolean;
|
||||
/** Omit to leave unchanged, null to clear a previously-set rank. */
|
||||
rankKey?: string | null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user