mirror of
https://github.com/Tria-plc/emaui.git
synced 2026-09-08 20:38:19 +00:00
Remove obsolete test artifacts and error context files for seafarer registration tests
This commit is contained in:
@@ -1,10 +1,23 @@
|
||||
import { Divider, Paper, Stack, Table, Text, Title } from "@mantine/core";
|
||||
import {
|
||||
Badge,
|
||||
Divider,
|
||||
Group,
|
||||
Grid,
|
||||
Paper,
|
||||
Stack,
|
||||
Text,
|
||||
Title,
|
||||
} from "@mantine/core";
|
||||
import {
|
||||
conditionHolds,
|
||||
displayFieldValue,
|
||||
type Attachment,
|
||||
type FormFieldConfig,
|
||||
type FormSectionConfig,
|
||||
type LicenseTypeRequirements,
|
||||
} from "@ema-platform/api";
|
||||
import { useDateDisplayer } from "@ema-platform/shared";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { DocumentSlots } from "./DocumentSlots";
|
||||
|
||||
interface Props {
|
||||
@@ -33,42 +46,83 @@ export function ApplicationSummary({
|
||||
attachments,
|
||||
applicationId,
|
||||
}: Props) {
|
||||
return (
|
||||
<Paper withBorder p="lg" radius="md">
|
||||
<Stack gap="lg">
|
||||
{sections.map((section) => (
|
||||
<div key={section.key}>
|
||||
<Text fw={600} fz="sm" tt="uppercase" c="gray.6" mb="sm">
|
||||
{localized(section.title)}
|
||||
</Text>
|
||||
<Table withTableBorder withColumnBorders>
|
||||
<Table.Tbody>
|
||||
{(section.fields ?? [])
|
||||
.filter((f) => conditionHolds(f.showWhen, formData))
|
||||
.map((field) => (
|
||||
<Table.Tr key={field.key}>
|
||||
<Table.Td w="45%">
|
||||
<Text size="xs" c="dimmed">
|
||||
{localized(field.label)}
|
||||
</Text>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Text size="sm">
|
||||
{String(formData[section.key]?.[field.key] ?? "—")}
|
||||
</Text>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
))}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
</div>
|
||||
))}
|
||||
const showDate = useDateDisplayer();
|
||||
const { i18n } = useTranslation();
|
||||
|
||||
<div>
|
||||
<Divider mb="md" />
|
||||
// Shared with the officer's review screen, so the applicant and the reviewer
|
||||
// never read the same answer two different ways.
|
||||
const display = (field: FormFieldConfig, raw: unknown) =>
|
||||
displayFieldValue(field, raw, {
|
||||
language: i18n.language,
|
||||
showDate,
|
||||
currency: config.feeCurrency,
|
||||
}) || "—";
|
||||
|
||||
return (
|
||||
<Stack gap="md">
|
||||
{sections.map((section) => {
|
||||
const fields = (section.fields ?? []).filter((f) =>
|
||||
conditionHolds(f.showWhen, formData),
|
||||
);
|
||||
if (fields.length === 0) return null;
|
||||
|
||||
return (
|
||||
<Paper withBorder p="lg" radius="md" key={section.key}>
|
||||
<Group justify="space-between" align="center" mb="xs">
|
||||
<Title order={5}>{localized(section.title)}</Title>
|
||||
<Badge variant="light" color="gray" size="sm">
|
||||
{fields.length} {fields.length === 1 ? "detail" : "details"}
|
||||
</Badge>
|
||||
</Group>
|
||||
{localized(section.description) && (
|
||||
<Text fz="xs" c="dimmed" mb="sm">
|
||||
{localized(section.description)}
|
||||
</Text>
|
||||
)}
|
||||
<Divider mb="md" />
|
||||
|
||||
{/* Label above value in two columns — a definition list reads far
|
||||
better than a bordered grid when most answers are short. */}
|
||||
<Grid gutter="md">
|
||||
{fields.map((field) => {
|
||||
const value = display(
|
||||
field,
|
||||
formData[section.key]?.[field.key],
|
||||
);
|
||||
const answered = value !== "—";
|
||||
return (
|
||||
<Grid.Col
|
||||
span={{
|
||||
base: 12,
|
||||
sm: field.type === "TEXTAREA" ? 12 : 6,
|
||||
}}
|
||||
key={field.key}
|
||||
>
|
||||
<Text fz="xs" c="dimmed" tt="uppercase" fw={600}>
|
||||
{localized(field.label)}
|
||||
</Text>
|
||||
<Text
|
||||
fz="sm"
|
||||
mt={2}
|
||||
c={answered ? undefined : "dimmed"}
|
||||
fs={answered ? undefined : "italic"}
|
||||
style={{ wordBreak: "break-word" }}
|
||||
>
|
||||
{answered ? value : "Not provided"}
|
||||
</Text>
|
||||
</Grid.Col>
|
||||
);
|
||||
})}
|
||||
</Grid>
|
||||
</Paper>
|
||||
);
|
||||
})}
|
||||
|
||||
<Paper withBorder p="lg" radius="md">
|
||||
<Title order={5} mb="sm">
|
||||
Documents
|
||||
</Title>
|
||||
<Divider mb="md" />
|
||||
<DocumentSlots
|
||||
requirements={config.documentRequirements}
|
||||
attachments={attachments}
|
||||
@@ -81,8 +135,7 @@ export function ApplicationSummary({
|
||||
// requires the callback.
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Paper>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
} from "@mantine/core";
|
||||
import {
|
||||
IconAlertTriangle,
|
||||
IconPencil,
|
||||
IconCheck,
|
||||
IconInfoCircle,
|
||||
IconPlus,
|
||||
@@ -309,7 +310,16 @@ export function LicenseApplicationPage() {
|
||||
);
|
||||
}
|
||||
|
||||
const readOnly = !["DRAFT", "RESUBMIT_REQUIRED"].includes(application.status);
|
||||
// A submitted application stays editable until an officer takes it, which
|
||||
// mirrors the server's own rule (`assertEditable`): an applicant who spots
|
||||
// their own mistake can fix it instead of waiting to be sent back for it.
|
||||
// Once claimed it locks — the officer reading it must not have the form move
|
||||
// underneath them.
|
||||
const editableWhileSubmitted =
|
||||
application.status === "SUBMITTED" && !application.assignedOfficerId;
|
||||
const readOnly =
|
||||
!["DRAFT", "RESUBMIT_REQUIRED"].includes(application.status) &&
|
||||
!editableWhileSubmitted;
|
||||
// A DRAFT has nothing worth summarising yet, so it always opens straight
|
||||
// into the wizard; every later status (including RESUBMIT_REQUIRED) opens
|
||||
// to the summary first.
|
||||
@@ -397,6 +407,7 @@ export function LicenseApplicationPage() {
|
||||
title: "Resubmitted",
|
||||
message: "Your corrections were sent back to the reviewing officer.",
|
||||
});
|
||||
navigate("/licensing/applications");
|
||||
} else {
|
||||
await submitApplication(appId as string).unwrap();
|
||||
notifications.show({
|
||||
@@ -404,8 +415,12 @@ export function LicenseApplicationPage() {
|
||||
title: "Application submitted",
|
||||
message: "You will be notified as it progresses.",
|
||||
});
|
||||
// Stays on the application rather than dropping the applicant into a
|
||||
// list: they have just filled a long form and the useful next screen is
|
||||
// what they submitted, with its status and — while it is still
|
||||
// unclaimed — the means to correct it.
|
||||
setViewingSummary(true);
|
||||
}
|
||||
navigate("/licensing/applications");
|
||||
} catch (err) {
|
||||
const found = extractValidationIssues(err);
|
||||
setIssues(found);
|
||||
@@ -571,10 +586,11 @@ export function LicenseApplicationPage() {
|
||||
<Text size="sm" c="dimmed">
|
||||
Fee: {config.fee ?? "—"} {config.feeCurrency}
|
||||
</Text>
|
||||
{showSummary && isAdjusting && (
|
||||
{showSummary && !readOnly && (
|
||||
<Button
|
||||
size="xs"
|
||||
variant="default"
|
||||
leftSection={<IconPencil size={14} />}
|
||||
onClick={() => setViewingSummary(false)}
|
||||
>
|
||||
Edit details
|
||||
@@ -603,6 +619,19 @@ export function LicenseApplicationPage() {
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{showSummary && editableWhileSubmitted && (
|
||||
<Alert
|
||||
color="blue"
|
||||
icon={<IconInfoCircle size={16} />}
|
||||
title="Submitted — still correctable"
|
||||
mb="md"
|
||||
>
|
||||
Your application is in the queue. You can still change any detail
|
||||
until a reviewing officer picks it up; after that, corrections happen
|
||||
only if they ask for them.
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{issues.length > 0 && (
|
||||
<Alert
|
||||
color="red"
|
||||
|
||||
@@ -30,14 +30,22 @@ import {
|
||||
IconX,
|
||||
} from '@tabler/icons-react';
|
||||
|
||||
interface ApplicationSummary {
|
||||
id: string;
|
||||
applicationId: string;
|
||||
status: string;
|
||||
submittedAt: string;
|
||||
}
|
||||
|
||||
/** The seaman-book page's whole state, as `/seaman-book/my` returns it. */
|
||||
interface SeamanBookOverview {
|
||||
application: {
|
||||
id: string;
|
||||
applicationId: string;
|
||||
status: string;
|
||||
submittedAt: string;
|
||||
} | null;
|
||||
application: ApplicationSummary | null;
|
||||
/**
|
||||
* The Basic Training Certificate opened alongside the book by an approved
|
||||
* seafarer registration — a separate application, separately numbered and
|
||||
* separately billed, so it is shown as its own card rather than merged in.
|
||||
*/
|
||||
btcApplication: ApplicationSummary | null;
|
||||
book: {
|
||||
id: string;
|
||||
issuedDate: string;
|
||||
@@ -122,6 +130,76 @@ function EligibilityItem({ label, ok }: { label: string; ok: boolean }) {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* One in-flight application: its number, where it stands, and the stages left.
|
||||
*
|
||||
* Shared by the Seaman Book and the BTC because an approved registration opens
|
||||
* both and they move independently — the book waits on a TRB inspection while
|
||||
* the BTC goes straight to payment, so a single merged card would have to lie
|
||||
* about one of them.
|
||||
*/
|
||||
function ApplicationCard({
|
||||
title,
|
||||
application,
|
||||
children,
|
||||
}: {
|
||||
title: string;
|
||||
application: ApplicationSummary;
|
||||
children?: React.ReactNode;
|
||||
}) {
|
||||
const activeStep = stageIndexFor(application.status);
|
||||
|
||||
return (
|
||||
<Paper withBorder radius="lg" p="lg">
|
||||
<Group justify="space-between" mb="md" wrap="wrap" gap="sm">
|
||||
<Group gap="xs">
|
||||
<ThemeIcon variant="light" color="blue" size={36} radius="md">
|
||||
<IconBook2 size={18} />
|
||||
</ThemeIcon>
|
||||
<div>
|
||||
<Text fw={700}>
|
||||
{title} — {application.id}
|
||||
</Text>
|
||||
<Text fz="xs" c="dimmed">
|
||||
{/* An approved seafarer registration opens this application as a
|
||||
draft, so it can be here before anyone has filed it. Calling
|
||||
that "Submitted" would misreport where it stands. */}
|
||||
{application.status === 'DRAFT' ? 'Opened' : 'Submitted'}{' '}
|
||||
{formatDate(application.submittedAt)}
|
||||
</Text>
|
||||
</div>
|
||||
</Group>
|
||||
<Badge
|
||||
color={STATUS_COLOR[application.status] ?? 'gray'}
|
||||
variant="light"
|
||||
size="lg"
|
||||
>
|
||||
{application.status.replaceAll('_', ' ')}
|
||||
</Badge>
|
||||
</Group>
|
||||
|
||||
<Stepper active={activeStep} size="sm" color="teal">
|
||||
{STAGES.map((stage, i) => (
|
||||
<Stepper.Step
|
||||
key={stage.label}
|
||||
label={stage.label}
|
||||
description={i <= activeStep ? 'Done' : 'Pending'}
|
||||
icon={
|
||||
i <= activeStep ? (
|
||||
<IconCircleCheck size={16} />
|
||||
) : (
|
||||
<IconClock size={16} />
|
||||
)
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</Stepper>
|
||||
|
||||
{children}
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Component
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -134,6 +212,7 @@ export function SeamanBookPage() {
|
||||
});
|
||||
|
||||
const application = data?.application ?? null;
|
||||
const btcApplication = data?.btcApplication ?? null;
|
||||
const eligibility = data?.eligibility;
|
||||
const bstItems = eligibility?.bstModules ?? [];
|
||||
const bstDone = bstItems.filter((b) => b.done).length;
|
||||
@@ -141,9 +220,10 @@ export function SeamanBookPage() {
|
||||
// The server decides: the same checklist gates the submission, so a screen
|
||||
// that judged eligibility for itself could offer a button the API refuses.
|
||||
const isEligible = data?.eligible ?? false;
|
||||
const submitted = Boolean(application);
|
||||
|
||||
const activeStep = stageIndexFor(application?.status);
|
||||
// Either service already being in flight means there is nothing to apply for
|
||||
// here — an approved registration opens both, so offering "Apply" alongside
|
||||
// them would invite a duplicate the server refuses anyway.
|
||||
const submitted = Boolean(application || btcApplication);
|
||||
|
||||
return (
|
||||
<Stack gap="md">
|
||||
@@ -156,59 +236,22 @@ export function SeamanBookPage() {
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
{/* Active application status */}
|
||||
{/* Active application status — one card per service in flight. */}
|
||||
{application && (
|
||||
<Paper withBorder radius="lg" p="lg">
|
||||
<Group justify="space-between" mb="md" wrap="wrap" gap="sm">
|
||||
<Group gap="xs">
|
||||
<ThemeIcon variant="light" color="blue" size={36} radius="md">
|
||||
<IconBook2 size={18} />
|
||||
</ThemeIcon>
|
||||
<div>
|
||||
<Text fw={700}>Application {application.id}</Text>
|
||||
<Text fz="xs" c="dimmed">
|
||||
{/* An approved seafarer registration opens this application
|
||||
as a draft, so it can be here before anyone has filed it.
|
||||
Calling that "Submitted" would misreport where it stands. */}
|
||||
{application.status === 'DRAFT' ? 'Opened' : 'Submitted'}{' '}
|
||||
{formatDate(application.submittedAt)}
|
||||
</Text>
|
||||
</div>
|
||||
</Group>
|
||||
<Badge
|
||||
color={STATUS_COLOR[application.status] ?? 'gray'}
|
||||
variant="light"
|
||||
size="lg"
|
||||
>
|
||||
{application.status.replaceAll('_', ' ')}
|
||||
</Badge>
|
||||
</Group>
|
||||
|
||||
{/* Progress stepper */}
|
||||
<Stepper active={activeStep} size="sm" color="teal">
|
||||
{STAGES.map((stage, i) => (
|
||||
<Stepper.Step
|
||||
key={stage.label}
|
||||
label={stage.label}
|
||||
description={i <= activeStep ? 'Done' : 'Pending'}
|
||||
icon={
|
||||
i <= activeStep ? (
|
||||
<IconCircleCheck size={16} />
|
||||
) : (
|
||||
<IconClock size={16} />
|
||||
)
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</Stepper>
|
||||
|
||||
<ApplicationCard title="Seaman Book" application={application}>
|
||||
{data?.book && (
|
||||
<Alert variant="light" color="teal" icon={<IconPrinter size={17} />} mt="md">
|
||||
Your Seaman Book <strong>{data.book.id}</strong> has been issued.
|
||||
Please visit the EMA office to collect it, bringing your National ID.
|
||||
</Alert>
|
||||
)}
|
||||
</Paper>
|
||||
</ApplicationCard>
|
||||
)}
|
||||
{btcApplication && (
|
||||
<ApplicationCard
|
||||
title="Basic Training Certificate"
|
||||
application={btcApplication}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* No active application — eligibility + apply */}
|
||||
|
||||
Reference in New Issue
Block a user