mirror of
https://github.com/Tria-plc/emaui.git
synced 2026-09-07 23:35:43 +00:00
Merge branch 'WorkflowChange' of https://github.com/Tria-plc/emaui into estif-branch-1
This commit is contained in:
@@ -407,23 +407,23 @@ export function LicenseApplicationPage() {
|
||||
const currentStep = steps[active];
|
||||
|
||||
/**
|
||||
* Checks the current step before moving on.
|
||||
* Checks one step before moving past it.
|
||||
*
|
||||
* The server rejects an incomplete application anyway, but only at submit —
|
||||
* by then the applicant has walked through every step and has to hunt for
|
||||
* what was missing. Validating per step points at the field directly.
|
||||
*
|
||||
* Takes the step rather than reading `currentStep`, so a jump ahead can
|
||||
* check each step it passes over instead of only the one being left.
|
||||
*/
|
||||
async function validateCurrentStep(): Promise<boolean> {
|
||||
async function validateStep(index: number): Promise<boolean> {
|
||||
const step = steps[index];
|
||||
// The wizard does not render until the configuration has loaded, but this
|
||||
// is declared above that guard, so narrow it here too.
|
||||
if (!currentStep || !config) return true;
|
||||
if (!step || !config) return true;
|
||||
|
||||
if (currentStep.kind === "sections") {
|
||||
const errors = validateSections(
|
||||
currentStep.sections,
|
||||
draft,
|
||||
i18n.language,
|
||||
);
|
||||
if (step.kind === "sections") {
|
||||
const errors = validateSections(step.sections, draft, i18n.language);
|
||||
setFieldErrors(errors);
|
||||
const count = Object.keys(errors).length;
|
||||
if (count > 0) {
|
||||
@@ -437,7 +437,7 @@ export function LicenseApplicationPage() {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (currentStep.kind === "staff") {
|
||||
if (step.kind === "staff") {
|
||||
const missing = config.staffRoleRequirements
|
||||
.filter(
|
||||
(role) =>
|
||||
@@ -461,7 +461,7 @@ export function LicenseApplicationPage() {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (currentStep.kind === "documents") {
|
||||
if (step.kind === "documents") {
|
||||
const supplied = new Set(
|
||||
attachments.filter((a) => a.files?.length).map((a) => a.documentKey),
|
||||
);
|
||||
@@ -495,6 +495,11 @@ export function LicenseApplicationPage() {
|
||||
return true;
|
||||
}
|
||||
|
||||
/** The step the applicant is on — what `Continue` validates. */
|
||||
async function validateCurrentStep(): Promise<boolean> {
|
||||
return validateStep(active);
|
||||
}
|
||||
|
||||
async function handleContinue() {
|
||||
// A locked step during an adjustment round has nothing to validate.
|
||||
if (!readOnly && !(await validateCurrentStep())) return;
|
||||
@@ -506,19 +511,39 @@ export function LicenseApplicationPage() {
|
||||
setActive((s) => Math.min(steps.length - 1, s + 1));
|
||||
}
|
||||
|
||||
/** Going back is always allowed; going forward validates each step passed. */
|
||||
/**
|
||||
* Going back is always allowed; going forward validates every step passed.
|
||||
*
|
||||
* `target` used to be discarded on the forward path — the handler validated
|
||||
* the current step and then advanced by exactly one, so clicking "4" from
|
||||
* step 1 landed on step 2. Two steps then showed the same content one click
|
||||
* apart, which reads as a broken wizard rather than a refused jump, and made
|
||||
* the later sections look absent entirely.
|
||||
*
|
||||
* Each step between here and `target` is validated and saved in order, so a
|
||||
* jump ahead cannot skip a required field the way a plain `setActive` would.
|
||||
* The walk stops at the first step that fails, leaving the applicant on it
|
||||
* with its errors showing.
|
||||
*/
|
||||
async function goToStep(target: number) {
|
||||
if (target <= active) {
|
||||
setActive(target);
|
||||
return;
|
||||
}
|
||||
if (!readOnly && !(await validateCurrentStep())) return;
|
||||
if (currentStep?.kind === "sections") {
|
||||
for (const section of currentStep.sections)
|
||||
await saveSection(section.key);
|
||||
|
||||
for (let step = active; step < target; step++) {
|
||||
if (!readOnly && !(await validateStep(step))) {
|
||||
setActive(step);
|
||||
return;
|
||||
}
|
||||
const passed = steps[step];
|
||||
if (passed?.kind === "sections") {
|
||||
for (const section of passed.sections) await saveSection(section.key);
|
||||
}
|
||||
}
|
||||
|
||||
setFieldErrors({});
|
||||
setActive(active + 1);
|
||||
setActive(target);
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -59,7 +59,7 @@ export function RequireSeafarerProfile({ children }: { children: React.ReactNode
|
||||
const { t } = useTranslation();
|
||||
const { typeCode } = useParams();
|
||||
const { pathname } = useLocation();
|
||||
const { isLoading, isFetching, error, gapsFor } = useCurrentProfile();
|
||||
const { isLoading, isFetching, error, gapsFor, profile } = useCurrentProfile();
|
||||
|
||||
// Shared wizard route — only the seafarer type is gated here.
|
||||
const gated = !typeCode || typeCode === REGISTRATION_TYPE_KEY;
|
||||
@@ -81,6 +81,15 @@ export function RequireSeafarerProfile({ children }: { children: React.ReactNode
|
||||
return <PageLoader label={t('profileGate.checkingProfile')} height={350} />;
|
||||
}
|
||||
|
||||
// Already registered: the number is permanent and the server now refuses a
|
||||
// second registration outright (409 seafarer_already_registered). Sending
|
||||
// them on beats opening a wizard whose first act — creating the draft — is
|
||||
// the call that fails. Checked after the loading guard so an unresolved
|
||||
// profile is never read as "not registered".
|
||||
if (profile?.seafarerNumber) {
|
||||
return <Navigate to="/seaman-book" replace />;
|
||||
}
|
||||
|
||||
// A failed lookup must not lock anyone out — the server still refuses the
|
||||
// application for a profile it can't fill in from.
|
||||
if (error) return <>{children}</>;
|
||||
|
||||
Reference in New Issue
Block a user