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

@@ -1,279 +0,0 @@
import {
Alert,
Badge,
Button,
Card,
Group,
List,
Loader,
Stack,
Table,
Text,
ThemeIcon,
Title,
} from '@mantine/core';
import {
IconArrowRight,
IconCertificate,
IconCircleCheck,
IconCircleX,
IconInfoCircle,
} from '@tabler/icons-react';
import { useNavigate } from 'react-router-dom';
import {
STATUS_COLORS,
STATUS_LABELS,
TERMINAL_STATUSES,
extractErrorMessage,
useGetCertificateUrlMutation,
useGetMyApplicationsQuery,
useGetMyLicensesQuery,
useGetMyMedicalCertificatesQuery,
useGetMySeaTimeQuery,
} from '@ema-platform/api';
import { useCurrentProfile } from '@ema-platform/auth';
import { notify } from '@ema-platform/ui';
const CERTIFICATE_TYPE_KEYS = [
'CERTIFICATE_OF_COMPETENCY',
'CERTIFICATE_OF_PROFICIENCY',
];
function EligibilityItem({ ok, label }: { ok: boolean; label: string }) {
return (
<List.Item
icon={
<ThemeIcon
color={ok ? 'teal' : 'red'}
variant="light"
size="sm"
radius="xl"
>
{ok ? <IconCircleCheck size={14} /> : <IconCircleX size={14} />}
</ThemeIcon>
}
>
{label}
</List.Item>
);
}
/**
* CoC / CoP home (US-CERT-002…004, 009): eligibility at a glance, the two
* application entry points, and the seafarer's certificate applications and
* issued certificates. The wizard itself is the config-driven licensing flow.
*/
export function CertificatesPage() {
const navigate = useNavigate();
const { profile, isLoading: loadingProfile } = useCurrentProfile();
const { data: seaTime } = useGetMySeaTimeQuery();
const { data: medicals } = useGetMyMedicalCertificatesQuery();
const { data: applications, isLoading: loadingApplications } =
useGetMyApplicationsQuery();
const { data: licenses } = useGetMyLicensesQuery();
const [getCertificateUrl] = useGetCertificateUrlMutation();
const registered =
Boolean(profile?.seafarerNumber) && profile?.seafarerStatus === 'ACTIVE';
const today = new Date().toISOString().slice(0, 10);
const hasMedical = (medicals ?? []).some(
(certificate) =>
certificate.status !== 'REJECTED' && certificate.expiryDate >= today,
);
const verifiedDays = seaTime?.totalDays ?? 0;
const certificateApplications = (applications?.items ?? []).filter((app) =>
CERTIFICATE_TYPE_KEYS.includes(app.licenseType?.key ?? ''),
);
const inFlight = certificateApplications.filter(
(app) => !TERMINAL_STATUSES.includes(app.status),
);
const issued = (licenses?.items ?? []).filter((license) =>
CERTIFICATE_TYPE_KEYS.includes(license.licenseType?.key ?? ''),
);
async function download(licenseId: string) {
try {
const result = await getCertificateUrl(licenseId).unwrap();
window.open(result.url, '_blank', 'noopener');
} catch (error) {
notify.error(extractErrorMessage(error, 'Could not fetch certificate'));
}
}
if (loadingProfile || loadingApplications) {
return (
<Group justify="center" py="xl">
<Loader />
</Group>
);
}
return (
<Stack maw={860} mx="auto">
<Title order={2}>My Certificates</Title>
<Card withBorder radius="md" p="lg">
<Group justify="space-between" align="flex-start">
<div>
<Text fw={600} mb={6}>
Eligibility
</Text>
<List spacing={4} size="sm">
<EligibilityItem
ok={registered}
label={
registered
? `Registered seafarer (${profile?.seafarerNumber})`
: 'Active seafarer registration required'
}
/>
<EligibilityItem
ok={hasMedical}
label={
hasMedical
? 'Current medical certificate on file'
: 'A current medical certificate is required'
}
/>
<EligibilityItem
ok={verifiedDays > 0}
label={`Verified sea time: ${verifiedDays} days (CoC needs 360, CoP 90)`}
/>
</List>
</div>
<Stack gap="xs">
<Button
rightSection={<IconArrowRight size={16} />}
onClick={() =>
navigate('/licensing/CERTIFICATE_OF_COMPETENCY/apply')
}
>
Apply for CoC
</Button>
<Button
variant="light"
rightSection={<IconArrowRight size={16} />}
onClick={() =>
navigate('/licensing/CERTIFICATE_OF_PROFICIENCY/apply')
}
>
Apply for CoP
</Button>
</Stack>
</Group>
{!registered && (
<Alert color="orange" mt="md" icon={<IconInfoCircle size={16} />}>
Complete your{' '}
<Text
component="span"
c="blue"
style={{ cursor: 'pointer' }}
onClick={() => navigate('/seafarer-registration')}
>
seafarer registration
</Text>{' '}
first certificate applications are refused without it.
</Alert>
)}
</Card>
{inFlight.length > 0 && (
<Stack gap="xs">
<Title order={4}>Applications in progress</Title>
{inFlight.map((app) => (
<Card key={app.id} withBorder radius="md" p="md">
<Group justify="space-between">
<div>
<Text fw={600}>{app.applicationNumber}</Text>
<Text size="xs" c="dimmed">
{app.licenseType?.name?.en}
</Text>
</div>
<Group>
<Badge color={STATUS_COLORS[app.status]}>
{STATUS_LABELS[app.status]}
</Badge>
<Button
size="compact-sm"
variant="light"
onClick={() =>
navigate(
`/licensing/${app.licenseType?.key}/applications/${app.id}`,
)
}
>
{app.status === 'DRAFT' || app.status === 'RESUBMIT_REQUIRED'
? 'Continue'
: 'View'}
</Button>
</Group>
</Group>
</Card>
))}
</Stack>
)}
<Stack gap="xs">
<Title order={4}>Issued certificates</Title>
{issued.length === 0 ? (
<Card withBorder radius="md" p="lg">
<Text size="sm" c="dimmed" ta="center">
No certificates issued yet.
</Text>
</Card>
) : (
<Table.ScrollContainer minWidth={640}>
<Table striped>
<Table.Thead>
<Table.Tr>
<Table.Th>Certificate </Table.Th>
<Table.Th>Type</Table.Th>
<Table.Th>Issued</Table.Th>
<Table.Th>Expires</Table.Th>
<Table.Th>Status</Table.Th>
<Table.Th />
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{issued.map((license) => (
<Table.Tr key={license.id}>
<Table.Td>
<Text ff="monospace" size="sm" fw={600}>
{license.certificateNumber}
</Text>
</Table.Td>
<Table.Td>{license.licenseType?.name?.en}</Table.Td>
<Table.Td>{license.issueDate?.slice(0, 10)}</Table.Td>
<Table.Td>{license.expiryDate?.slice(0, 10)}</Table.Td>
<Table.Td>
<Badge
size="sm"
variant="light"
color={license.status === 'ACTIVE' ? 'green' : 'red'}
>
{license.status}
</Badge>
</Table.Td>
<Table.Td>
<Button
size="compact-xs"
variant="subtle"
leftSection={<IconCertificate size={14} />}
onClick={() => download(license.id)}
>
Download
</Button>
</Table.Td>
</Table.Tr>
))}
</Table.Tbody>
</Table>
</Table.ScrollContainer>
)}
</Stack>
</Stack>
);
}
export default CertificatesPage;