mirror of
https://github.com/Tria-plc/emaui.git
synced 2026-08-26 19:12:50 +00:00
- Added RequirePermission component to manage access based on user permissions. - Integrated permission checks in MySeaRecordsPage, SeafarerRegistrationPage, VesselRegistrationPage, WaiverPage, and PortalLayout. - Updated router to enforce permissions for specific routes. - Introduced PORTAL_PERMISSIONS and LICENSE_PERMISSIONS constants for consistent permission management. - Enhanced usePermissions hook to fetch permissions from the server and determine access rights. - Refactored UI components to conditionally render based on user permissions, improving security and user experience.
269 lines
8.7 KiB
TypeScript
269 lines
8.7 KiB
TypeScript
import {
|
|
Alert,
|
|
Badge,
|
|
Button,
|
|
Card,
|
|
Group,
|
|
List,
|
|
Loader,
|
|
Paper,
|
|
Progress,
|
|
Stack,
|
|
Text,
|
|
Title,
|
|
} from '@mantine/core';
|
|
import {
|
|
IconAnchor,
|
|
IconArrowRight,
|
|
IconCircleCheck,
|
|
IconClipboardList,
|
|
IconInfoCircle,
|
|
} from '@tabler/icons-react';
|
|
import { useMemo } from 'react';
|
|
import { useNavigate } from 'react-router-dom';
|
|
import {
|
|
STATUS_COLORS,
|
|
STATUS_LABELS,
|
|
STATUS_PROGRESS,
|
|
TERMINAL_STATUSES,
|
|
useGetMyApplicationsQuery,
|
|
} from '@ema-platform/api';
|
|
import {
|
|
PORTAL_PERMISSIONS,
|
|
RequirePermission,
|
|
useCurrentProfile,
|
|
} from '@ema-platform/auth';
|
|
|
|
const REGISTRATION_TYPE_KEY = 'SEAFARER_REGISTRATION';
|
|
|
|
const DEPARTMENT_LABELS: Record<string, string> = {
|
|
DECK: 'Deck',
|
|
ENGINE: 'Engine',
|
|
CATERING: 'Catering',
|
|
};
|
|
|
|
const SEAFARER_STATUS_COLORS: Record<string, string> = {
|
|
ACTIVE: 'green',
|
|
PENDING: 'yellow',
|
|
SUSPENDED: 'orange',
|
|
INACTIVE: 'gray',
|
|
};
|
|
|
|
/**
|
|
* The seafarer's registration home (US-SEA-001…007).
|
|
*
|
|
* Registration itself runs through the config-driven licensing wizard — this
|
|
* page is the state machine around it: start a registration, resume or track
|
|
* the one in flight, or show the registered identity once approved.
|
|
*/
|
|
export function SeafarerRegistrationPage() {
|
|
const navigate = useNavigate();
|
|
const { profile, isLoading: loadingProfile } = useCurrentProfile();
|
|
const { data: applications, isLoading: loadingApplications } =
|
|
useGetMyApplicationsQuery();
|
|
|
|
// The latest registration application, in flight or decided.
|
|
const registration = useMemo(() => {
|
|
const mine = (applications?.items ?? []).filter(
|
|
(app) => app.licenseType?.key === REGISTRATION_TYPE_KEY,
|
|
);
|
|
return mine.sort(
|
|
(a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(),
|
|
)[0];
|
|
}, [applications]);
|
|
|
|
if (loadingProfile || loadingApplications) {
|
|
return (
|
|
<Group justify="center" py="xl">
|
|
<Loader />
|
|
</Group>
|
|
);
|
|
}
|
|
|
|
// ------------------------------------------------------------- registered
|
|
if (profile?.seafarerNumber) {
|
|
const status = profile.seafarerStatus ?? 'ACTIVE';
|
|
return (
|
|
<Stack maw={720} mx="auto">
|
|
<Title order={2}>Seafarer Registration</Title>
|
|
<Card withBorder radius="md" p="xl">
|
|
<Stack>
|
|
<Group justify="space-between">
|
|
<Group>
|
|
<IconCircleCheck size={32} color="var(--mantine-color-green-6)" />
|
|
<div>
|
|
<Text fw={700} size="lg">
|
|
Registered Seafarer
|
|
</Text>
|
|
<Text c="dimmed" size="sm">
|
|
Your official seafarer profile with the Ethiopian Maritime
|
|
Authority.
|
|
</Text>
|
|
</div>
|
|
</Group>
|
|
<Badge color={SEAFARER_STATUS_COLORS[status] ?? 'gray'} size="lg">
|
|
{status}
|
|
</Badge>
|
|
</Group>
|
|
<Group gap="xl" mt="sm">
|
|
<div>
|
|
<Text size="xs" c="dimmed" tt="uppercase">
|
|
Seafarer Number
|
|
</Text>
|
|
<Text fw={700} ff="monospace" size="lg">
|
|
{profile.seafarerNumber}
|
|
</Text>
|
|
</div>
|
|
{profile.seafarerDepartment && (
|
|
<div>
|
|
<Text size="xs" c="dimmed" tt="uppercase">
|
|
Department
|
|
</Text>
|
|
<Text fw={600}>
|
|
{DEPARTMENT_LABELS[profile.seafarerDepartment] ??
|
|
profile.seafarerDepartment}
|
|
</Text>
|
|
</div>
|
|
)}
|
|
</Group>
|
|
{status === 'SUSPENDED' && profile.seafarerStatusReason && (
|
|
<Alert color="orange" icon={<IconInfoCircle size={16} />}>
|
|
Your profile is suspended: {profile.seafarerStatusReason}
|
|
</Alert>
|
|
)}
|
|
</Stack>
|
|
</Card>
|
|
<Paper withBorder radius="md" p="lg">
|
|
<Group justify="space-between">
|
|
<div>
|
|
<Text fw={600}>Sea service & medical records</Text>
|
|
<Text size="sm" c="dimmed">
|
|
Keep your sea-service history and medical certificates up to
|
|
date — certificate and seaman-book applications draw on them.
|
|
</Text>
|
|
</div>
|
|
<Button
|
|
variant="light"
|
|
rightSection={<IconArrowRight size={16} />}
|
|
onClick={() => navigate('/seafarer/records')}
|
|
>
|
|
My records
|
|
</Button>
|
|
</Group>
|
|
</Paper>
|
|
</Stack>
|
|
);
|
|
}
|
|
|
|
// -------------------------------------------------------------- in flight
|
|
if (registration && !TERMINAL_STATUSES.includes(registration.status)) {
|
|
const isDraft = registration.status === 'DRAFT';
|
|
const needsAction = registration.status === 'RESUBMIT_REQUIRED';
|
|
return (
|
|
<Stack maw={720} mx="auto">
|
|
<Title order={2}>Seafarer Registration</Title>
|
|
<Card withBorder radius="md" p="xl">
|
|
<Stack>
|
|
<Group justify="space-between">
|
|
<div>
|
|
<Text fw={700}>{registration.applicationNumber}</Text>
|
|
<Text size="sm" c="dimmed">
|
|
Submitted registrations are reviewed by an EMA registration
|
|
officer; you will be notified of every decision.
|
|
</Text>
|
|
</div>
|
|
<Badge color={STATUS_COLORS[registration.status]} size="lg">
|
|
{STATUS_LABELS[registration.status]}
|
|
</Badge>
|
|
</Group>
|
|
<Progress value={STATUS_PROGRESS[registration.status]} />
|
|
{needsAction && (
|
|
<Alert color="orange" icon={<IconInfoCircle size={16} />}>
|
|
The registration officer asked for corrections. Open the
|
|
application to see exactly what needs fixing.
|
|
</Alert>
|
|
)}
|
|
<Group>
|
|
<Button
|
|
rightSection={<IconArrowRight size={16} />}
|
|
onClick={() =>
|
|
navigate(
|
|
isDraft || needsAction
|
|
? `/licensing/${REGISTRATION_TYPE_KEY}/apply`
|
|
: `/licensing/${REGISTRATION_TYPE_KEY}/applications/${registration.id}`,
|
|
)
|
|
}
|
|
>
|
|
{isDraft
|
|
? 'Continue registration'
|
|
: needsAction
|
|
? 'Fix and resubmit'
|
|
: 'View application'}
|
|
</Button>
|
|
</Group>
|
|
</Stack>
|
|
</Card>
|
|
</Stack>
|
|
);
|
|
}
|
|
|
|
// ------------------------------------------------------- not yet started
|
|
return (
|
|
<Stack maw={720} mx="auto">
|
|
<Title order={2}>Seafarer Registration</Title>
|
|
{registration?.status === 'REJECTED' && (
|
|
<Alert color="red" title="Previous registration rejected">
|
|
{registration.rejectionReason ??
|
|
'Your previous registration was rejected. You may register again.'}
|
|
</Alert>
|
|
)}
|
|
<Card withBorder radius="md" p="xl">
|
|
<Stack>
|
|
<Group>
|
|
<IconAnchor size={32} color="var(--mantine-color-blue-6)" />
|
|
<div>
|
|
<Text fw={700} size="lg">
|
|
Register as a seafarer
|
|
</Text>
|
|
<Text c="dimmed" size="sm">
|
|
Approval creates your official seafarer profile with a unique
|
|
seafarer number — the identity every maritime service builds on.
|
|
</Text>
|
|
</div>
|
|
</Group>
|
|
<Text fw={600} size="sm" mt="sm">
|
|
You will need:
|
|
</Text>
|
|
<List
|
|
size="sm"
|
|
spacing={4}
|
|
icon={<IconClipboardList size={16} color="var(--mantine-color-blue-5)" />}
|
|
>
|
|
<List.Item>A passport-size photograph</List.Item>
|
|
<List.Item>Your National ID (Fayda) or Kebele ID</List.Item>
|
|
<List.Item>Your educational certificate</List.Item>
|
|
<List.Item>
|
|
A medical fitness certificate and passport, if you already hold
|
|
them
|
|
</List.Item>
|
|
</List>
|
|
<Group mt="md">
|
|
<RequirePermission
|
|
anyOf={[PORTAL_PERMISSIONS.APPLY_SEAFARER_REGISTRATION]}
|
|
hideOnly
|
|
>
|
|
<Button
|
|
size="md"
|
|
rightSection={<IconArrowRight size={18} />}
|
|
onClick={() => navigate(`/licensing/${REGISTRATION_TYPE_KEY}/apply`)}
|
|
>
|
|
Start registration
|
|
</Button>
|
|
</RequirePermission>
|
|
</Group>
|
|
</Stack>
|
|
</Card>
|
|
</Stack>
|
|
);
|
|
}
|