feat: add read-only SeafarerScopeFields component to profile page for displaying certification scope details

This commit is contained in:
estifanos
2026-09-08 09:52:24 +00:00
parent f11ccb958d
commit a76ad9479f
4 changed files with 130 additions and 0 deletions

View File

@@ -0,0 +1,92 @@
import { Paper, SimpleGrid, Stack, Text, TextInput, Title } from '@mantine/core';
import { useTranslation } from 'react-i18next';
import {
displaySeafarerAnswer,
useGetActiveDepartmentsQuery,
useGetMySeafarerRegistrationQuery,
useLocalized,
} from '@ema-platform/api';
import { PORTAL_PERMISSIONS, usePermissions } from '@ema-platform/auth';
/**
* Department and STCW limitation, read-only.
*
* Answered once in the registration wizard and never again, yet between them
* they decide which certificates, examinations and services the portal offers
* — so a seafarer has to be able to see what they are without reopening a
* submitted registration. Not editable here on purpose: either answer moves
* the applicant onto a different CoC ladder, which is EMA's decision on
* review, not a profile edit.
*
* `tier` lives only on the registration record — the profile carries the
* department alone (`seafarerDepartment`) — so the registration is the source
* for both, with the profile's department as the fallback for an account whose
* registration is not readable.
*/
export function SeafarerScopeFields({
fallbackDepartment,
}: {
/** `profile.seafarerDepartment`, used when no registration comes back. */
fallbackDepartment?: string | null;
}) {
const { t } = useTranslation();
const localized = useLocalized();
// Same permission the registration page and `/seafarer-registrations/mine`
// are behind — asking without it would only 403.
const { can } = usePermissions();
const mayRegister = can([PORTAL_PERMISSIONS.APPLY_SEAFARER_REGISTRATION]);
const { data, isLoading } = useGetMySeafarerRegistrationQuery(undefined, {
skip: !mayRegister,
});
const registration = data?.registration ?? null;
const department = registration?.department ?? fallbackDepartment ?? null;
const tier = registration?.tier ?? null;
// Live department names, so a department configured after this release still
// shows its own name — the same list the registration wizard picks from.
const { data: departments } = useGetActiveDepartmentsQuery(undefined, { skip: !department });
const departmentOptions = departments?.map((d) => ({ value: d.code, label: localized(d.name) }));
// Nothing recorded yet — the registration wizard is where these are answered.
if (isLoading || (!department && !tier)) return null;
return (
<Paper p="xl" mt="md" shadow="sm" radius="lg" withBorder>
<Stack gap="md">
<div>
<Title order={5}>{t('profile.seafarerScope.title')}</Title>
<Text size="sm" c="dimmed">
{t('profile.seafarerScope.subtitle')}
</Text>
</div>
<SimpleGrid cols={{ base: 1, sm: 2 }} spacing="md">
<TextInput
label={t('profile.seafarerScope.department')}
readOnly
variant="filled"
value={department ? displaySeafarerAnswer('department', department, departmentOptions) : '—'}
/>
<TextInput
label={t('profile.seafarerScope.tier')}
readOnly
variant="filled"
value={tier ? t(`profile.seafarerScope.tiers.${tier}`) : '—'}
description={
tier
? department === 'ENGINE'
? t('profile.seafarerScope.tierHintEngine')
: t('profile.seafarerScope.tierHintDeck')
: undefined
}
/>
</SimpleGrid>
{registration && registration.status !== 'APPROVED' && (
<Text size="xs" c="dimmed">
{t('profile.seafarerScope.pending')}
</Text>
)}
</Stack>
</Paper>
);
}

View File

@@ -68,6 +68,7 @@ import {
import { useSaveMyAddressMutation } from '../api/address-api';
import { toAddressPayload } from '../types/address';
import { OperationsFormContent } from '../components/OperationsFormContent';
import { SeafarerScopeFields } from '../components/SeafarerScopeFields';
import { MySignaturePad } from '../components/SignaturePad';
import { SEAFARER_PROFILE_REQUIREMENT } from '../components/RequireSeafarerProfile';
import classes from './ProfilePage.module.css';
@@ -768,6 +769,12 @@ export function ProfilePage() {
</form>
)}
</Paper>
{/* Department and STCW limitation — answered at registration, shown
here because they decide which certificates apply. */}
<SeafarerScopeFields
fallbackDepartment={(resolvedProfile ?? storedProfile)?.seafarerDepartment}
/>
</Tabs.Panel>
{/* ---- Address ---- */}

View File

@@ -464,6 +464,21 @@ export const am: Translations = {
noProfile: 'ምንም መገለጫ አልተገኘም። መጀመሪያ መገለጫዎን ያጠናቅቁ።',
save: 'መገለጫ አስቀምጥ',
},
seafarerScope: {
title: 'የምስክር ወረቀት ወሰን',
subtitle:
'እንደ መርከበኛ በተመዘገቡበት ጊዜ የተመዘገበ። አንድ ላይ የትኞቹ የምስክር ወረቀቶች፣ ፈተናዎችና አገልግሎቶች እንደሚመለከቱዎት ይወስናሉ — ለመቀየር EMAን ያነጋግሩ።',
department: 'ክፍል',
tier: 'የማዕረግ ደረጃ',
tiers: {
ABOVE: 'ከላይ',
BELOW: 'ከታች',
},
tierHintDeck: 'ከላይ 3,000 ጠቅላላ ቶን እና ከዚያ በላይ የሆኑ መርከቦችን ይሸፍናል፤ ከታች ከ5003,000 ጠቅላላ ቶን ይሸፍናል።',
tierHintEngine:
'ከላይ 3,000 ኪ.ዋ እና ከዚያ በላይ የማንቀሳቀሻ ኃይል ያላቸውን መርከቦች ይሸፍናል፤ ከታች ከ7503,000 ኪ.ዋ ይሸፍናል።',
pending: 'ገና ካልጸደቀው የመርከበኛ ምዝገባዎ የተወሰደ።',
},
addressSection: {
title: 'አድራሻ እና መገናኛ',
subtitle: 'የመታወቂያ ሰነዶችዎ፣ የመገናኛ ዝርዝሮችዎ እና የአደጋ ጊዜ ተጠሪ',

View File

@@ -470,6 +470,22 @@ export const en = {
noProfile: 'No profile found. Complete your profile setup first.',
save: 'Save Profile',
},
seafarerScope: {
title: 'Certification Scope',
subtitle:
'Recorded when you registered as a seafarer. Together they decide which certificates, examinations and services apply to you — contact EMA to change either.',
department: 'Department',
tier: 'Certificate Limitation',
tiers: {
ABOVE: 'Above',
BELOW: 'Below',
},
tierHintDeck:
'Above covers ships of 3,000 gross tonnage or more; Below covers 5003,000 GT.',
tierHintEngine:
'Above covers ships of 3,000 kW propulsion power or more; Below covers 7503,000 kW.',
pending: 'From your seafarer registration, which is not approved yet.',
},
addressSection: {
title: 'Address & Contact',
subtitle: 'Your identity documents, contact details and emergency contact',