feat(vessel-registration): add vessel registration page with incident reporting and certificate download functionality

feat(waiver): implement waiver page with application tracking and letter download feature

feat(ui): introduce AdvancedTable component for enhanced table functionality across the application

chore: update package-lock.json to remove unnecessary dependencies
This commit is contained in:
Nati
2026-08-13 09:11:50 +00:00
parent a7e74cb7bd
commit 2af9e60800
67 changed files with 4209 additions and 3446 deletions

View File

@@ -0,0 +1,116 @@
import { ActionIcon, Badge, Button, Group, Text } from '@mantine/core';
import { IconEdit, IconGavel, IconSend, IconTrash } from '@tabler/icons-react';
import type { TFunction } from 'i18next';
import type { AdvancedTableColumn } from '@ema-platform/ui';
import type { Question, QuestionStatus } from '../../types/question';
/** Bank items travel DRAFT → PENDING_APPROVAL → APPROVED (US-EXAM-003). */
const QC_COLOR: Record<QuestionStatus, string> = {
DRAFT: 'gray',
PENDING_APPROVAL: 'yellow',
APPROVED: 'teal',
REJECTED: 'red',
RETIRED: 'dark',
};
/** QC actions are text buttons by design — kept as a rendered column. */
export function questionColumns(
t: TFunction,
handlers: {
locale: 'en' | 'am';
getCertName: (id: string) => string;
isSubmittingReview: boolean;
onSubmitForApproval: (question: Question) => void;
onReview: (question: Question, outcome: 'APPROVED' | 'REJECTED' | 'RETIRED') => void;
onEdit: (question: Question) => void;
onDelete: (question: Question) => void;
},
): AdvancedTableColumn<Question>[] {
return [
{
key: 'title',
header: t('question.columns.title'),
render: (q) => <Text fz="sm" maw={300} lineClamp={2}>{q.title[handlers.locale]}</Text>,
},
{
key: 'certification',
header: t('question.columns.certification'),
render: (q) => <Text fz="sm">{handlers.getCertName(q.certificationId)}</Text>,
},
{
key: 'form',
header: t('question.columns.form'),
render: (q) => <Badge size="sm" variant="light" color={q.form === 'ESSAY' ? 'blue' : 'violet'}>{t(`question.form.${q.form === 'ESSAY' ? 'essay' : 'choice'}`)}</Badge>,
},
{
key: 'points',
header: t('question.columns.points'),
render: (q) => <Text fz="sm" fw={600}>{q.points}</Text>,
},
{
key: 'status',
header: t('question.qc.column'),
render: (q) => (
<Badge size="sm" variant="light" color={QC_COLOR[q.status] ?? 'gray'} title={q.reviewRemark ?? undefined}>
{t(`question.qc.${q.status}`)}
</Badge>
),
},
{
key: 'actions',
header: '',
render: (q) => (
<Group gap="xs">
{(q.status === 'DRAFT' || q.status === 'REJECTED') && (
<Button
size="compact-xs"
variant="light"
leftSection={<IconSend size={12} />}
loading={handlers.isSubmittingReview}
onClick={() => handlers.onSubmitForApproval(q)}
>
{t('question.qc.submit')}
</Button>
)}
{q.status === 'PENDING_APPROVAL' && (
<>
<Button
size="compact-xs"
variant="light"
color="teal"
onClick={() => handlers.onReview(q, 'APPROVED')}
>
{t('question.qc.approve')}
</Button>
<Button
size="compact-xs"
variant="light"
color="red"
onClick={() => handlers.onReview(q, 'REJECTED')}
>
{t('question.qc.reject')}
</Button>
</>
)}
{q.status === 'APPROVED' && (
<Button
size="compact-xs"
variant="subtle"
color="dark"
leftSection={<IconGavel size={12} />}
onClick={() => handlers.onReview(q, 'RETIRED')}
>
{t('question.qc.retire')}
</Button>
)}
<ActionIcon variant="subtle" color="blue" size="sm" onClick={() => handlers.onEdit(q)}>
<IconEdit size={14} />
</ActionIcon>
<ActionIcon variant="subtle" color="red" size="sm" onClick={() => handlers.onDelete(q)}>
<IconTrash size={14} />
</ActionIcon>
</Group>
),
},
];
}

View File

@@ -4,9 +4,7 @@ import {
Title,
Group,
Button,
Table,
Badge,
ActionIcon,
Modal,
Text,
TextInput,
@@ -20,17 +18,10 @@ import {
} from '@mantine/core';
import { useDisclosure } from '@mantine/hooks';
import { useTranslation } from 'react-i18next';
import {
IconEdit,
IconTrash,
IconPlus,
IconInfoCircle,
IconSend,
IconGavel,
} from '@tabler/icons-react';
import { notify } from '@ema-platform/ui';
import { IconPlus, IconInfoCircle } from '@tabler/icons-react';
import { AdvancedTable, notify } from '@ema-platform/ui';
import { extractErrorMessage } from '@ema-platform/api';
import { useGetCertificationsQuery } from '../../certification/api/certification-api';
import { useGetCertificationsQuery } from '../../../certification/api/certification-api';
import {
useGetQuestionsQuery,
useCreateQuestionMutation,
@@ -38,17 +29,9 @@ import {
useDeleteQuestionMutation,
useSubmitQuestionMutation,
useReviewQuestionMutation,
} from '../api/question-api';
import type { Question, QuestionForm, QuestionStatus } from '../types/question';
/** Bank items travel DRAFT → PENDING_APPROVAL → APPROVED (US-EXAM-003). */
const QC_COLOR: Record<QuestionStatus, string> = {
DRAFT: 'gray',
PENDING_APPROVAL: 'yellow',
APPROVED: 'teal',
REJECTED: 'red',
RETIRED: 'dark',
};
} from '../../api/question-api';
import type { Question, QuestionForm } from '../../types/question';
import { questionColumns } from './columns';
function QuestionForm({
editing,
@@ -123,7 +106,7 @@ export function QuestionPage() {
const { t, i18n } = useTranslation();
const locale = i18n.language as 'en' | 'am';
const { data: certRes } = useGetCertificationsQuery();
const { data, isLoading, isError } = useGetQuestionsQuery();
const { data, isLoading, isError, refetch } = useGetQuestionsQuery();
const [createQ, { isLoading: isCreating }] = useCreateQuestionMutation();
const [updateQ, { isLoading: isUpdating }] = useUpdateQuestionMutation();
const [deleteQ] = useDeleteQuestionMutation();
@@ -245,92 +228,21 @@ export function QuestionPage() {
<Text fw={600}>{t('question.pool')}</Text>
<Select placeholder={t('question.filterByCertification')} data={[{ value: '', label: 'All' }, ...certOptions]} value={certFilter} onChange={(v) => setCertFilter(v ?? null)} size="sm" style={{ width: 280 }} clearable />
</Group>
<Table striped highlightOnHover>
<Table.Thead bg="var(--mantine-color-default-hover)">
<Table.Tr>
<Table.Th>{t('question.columns.title')}</Table.Th>
<Table.Th>{t('question.columns.certification')}</Table.Th>
<Table.Th>{t('question.columns.form')}</Table.Th>
<Table.Th>{t('question.columns.points')}</Table.Th>
<Table.Th>{t('question.qc.column')}</Table.Th>
<Table.Th />
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{filtered.map((q) => (
<Table.Tr key={q.id}>
<Table.Td><Text fz="sm" maw={300} lineClamp={2}>{q.title[locale]}</Text></Table.Td>
<Table.Td><Text fz="sm">{getCertName(q.certificationId)}</Text></Table.Td>
<Table.Td><Badge size="sm" variant="light" color={q.form === 'ESSAY' ? 'blue' : 'violet'}>{t(`question.form.${q.form === 'ESSAY' ? 'essay' : 'choice'}`)}</Badge></Table.Td>
<Table.Td><Text fz="sm" fw={600}>{q.points}</Text></Table.Td>
<Table.Td>
<Badge size="sm" variant="light" color={QC_COLOR[q.status] ?? 'gray'} title={q.reviewRemark ?? undefined}>
{t(`question.qc.${q.status}`)}
</Badge>
</Table.Td>
<Table.Td>
<Group gap="xs">
{(q.status === 'DRAFT' || q.status === 'REJECTED') && (
<Button
size="compact-xs"
variant="light"
leftSection={<IconSend size={12} />}
loading={isSubmittingReview}
onClick={() => handleSubmitForApproval(q)}
>
{t('question.qc.submit')}
</Button>
)}
{q.status === 'PENDING_APPROVAL' && (
<>
<Button
size="compact-xs"
variant="light"
color="teal"
onClick={() => openReview(q, 'APPROVED')}
>
{t('question.qc.approve')}
</Button>
<Button
size="compact-xs"
variant="light"
color="red"
onClick={() => openReview(q, 'REJECTED')}
>
{t('question.qc.reject')}
</Button>
</>
)}
{q.status === 'APPROVED' && (
<Button
size="compact-xs"
variant="subtle"
color="dark"
leftSection={<IconGavel size={12} />}
onClick={() => openReview(q, 'RETIRED')}
>
{t('question.qc.retire')}
</Button>
)}
<ActionIcon variant="subtle" color="blue" size="sm" onClick={() => { setEditing(q); setShowForm(true); }}>
<IconEdit size={14} />
</ActionIcon>
<ActionIcon variant="subtle" color="red" size="sm" onClick={() => { setDeleteTarget(q); openDelete(); }}>
<IconTrash size={14} />
</ActionIcon>
</Group>
</Table.Td>
</Table.Tr>
))}
{filtered.length === 0 && (
<Table.Tr>
<Table.Td colSpan={6}>
<Text c="dimmed" ta="center" py="xl">{t('question.noQuestions')}</Text>
</Table.Td>
</Table.Tr>
)}
</Table.Tbody>
</Table>
<AdvancedTable
columns={questionColumns(t, {
locale,
getCertName,
isSubmittingReview,
onSubmitForApproval: handleSubmitForApproval,
onReview: openReview,
onEdit: (q) => { setEditing(q); setShowForm(true); },
onDelete: (q) => { setDeleteTarget(q); openDelete(); },
})}
data={filtered}
rowKey={(q) => q.id}
onRefresh={refetch}
emptyTitle={t('question.noQuestions')}
/>
</Paper>
<Modal