mirror of
https://github.com/Tria-plc/emaui.git
synced 2026-08-26 13:02:50 +00:00
feat: update seafarer registration flow and localization; remove profile gating and enhance user experience
This commit is contained in:
@@ -23,7 +23,12 @@ import { approveRegistration, runWorkflow } from './support/workflow';
|
||||
*/
|
||||
|
||||
/**
|
||||
* Fills the fields `RequireSeafarerProfile` refuses to open the wizard without.
|
||||
* Fills the profile the seafarer wizard prefills its Identity Details step
|
||||
* from.
|
||||
*
|
||||
* No longer a precondition for reaching the wizard — that redirect is gone and
|
||||
* the step collects these itself — but a populated profile is the returning
|
||||
* applicant's case, and it is the prefill that keeps them from retyping.
|
||||
*
|
||||
* They are split across two tabs, and every tab's panel is in the DOM whether
|
||||
* or not it is showing — so each one has to be selected before its inputs can
|
||||
|
||||
@@ -227,12 +227,17 @@ export function LicenseApplicationPage() {
|
||||
detail?.application?.formData,
|
||||
]);
|
||||
|
||||
// Generic fill for every field the config marks `readOnly` with a
|
||||
// `source` — e.g. seafarer registration's read-only Identity Details step,
|
||||
// which shows what's already on the profile instead of asking again.
|
||||
// `readOnly` fields are never sent by the applicant and the server skips
|
||||
// them at validation, so this is display-only; the profile itself is what
|
||||
// an edit has to go through.
|
||||
// Generic fill for every field the config gives a `source` — the profile
|
||||
// value the applicant would otherwise retype. Seafarer registration's
|
||||
// Identity Details step is the case that drives this: it collects name,
|
||||
// gender, DOB and national ID *in the wizard* rather than sending the
|
||||
// applicant to `/profile` first, so those fields are editable and this is a
|
||||
// prefill, not a display.
|
||||
//
|
||||
// Editable sourced fields are filled only while still blank. Re-running
|
||||
// this effect (a refetched profile, a saved draft) must not overwrite what
|
||||
// the applicant has since typed — for a `readOnly` field the profile stays
|
||||
// authoritative, so those keep tracking it.
|
||||
useEffect(() => {
|
||||
if (!profile || !config) return;
|
||||
const context = { user: profile.user, profile };
|
||||
@@ -242,10 +247,14 @@ export function LicenseApplicationPage() {
|
||||
const next = { ...prev };
|
||||
for (const section of config.licenseType.formSchema.sections) {
|
||||
for (const field of section.fields) {
|
||||
if (!field.readOnly || !field.source) continue;
|
||||
if (!field.source) continue;
|
||||
const current = next[section.key]?.[field.key];
|
||||
const untouched =
|
||||
current === undefined || current === null || current === "";
|
||||
if (!field.readOnly && !untouched) continue;
|
||||
const value = readSourcePath(context, field.source);
|
||||
if (value === undefined || value === null || value === "") continue;
|
||||
if (next[section.key]?.[field.key] === value) continue;
|
||||
if (current === value) continue;
|
||||
next[section.key] = { ...next[section.key], [field.key]: value };
|
||||
changed = true;
|
||||
}
|
||||
|
||||
@@ -1,24 +1,14 @@
|
||||
import { useEffect } from 'react';
|
||||
import { Center, Loader } from '@mantine/core';
|
||||
import { Navigate, useLocation, useParams } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { notify } from '@ema-platform/ui';
|
||||
import {
|
||||
PROFILE_FIELD_SECTION,
|
||||
useCurrentProfile,
|
||||
type ProfileRequirement,
|
||||
} from '@ema-platform/auth';
|
||||
import { Navigate, useParams } from 'react-router-dom';
|
||||
import { useCurrentProfile, type ProfileRequirement } from '@ema-platform/auth';
|
||||
|
||||
/**
|
||||
* Seafarer registration is filled in from the profile (nationality, ID,
|
||||
* names, contact details) — the server refuses an application missing them,
|
||||
* so they're asked for up front instead of at submit time.
|
||||
* The identity the seafarer wizard needs before it can produce a registration.
|
||||
*
|
||||
* Only the fields the Personal, Maritime Profile and Address tabs actually
|
||||
* mark required — matches `profileSchema` / `addressSchema`, so the gate is
|
||||
* always satisfiable by finishing those tabs and never blocks on an optional
|
||||
* field (place of birth, region/city/woreda, emergency contact) the forms
|
||||
* don't star.
|
||||
* No longer a gate on opening the wizard: the Identity Details step collects
|
||||
* these itself, so an applicant with an empty profile starts in registration
|
||||
* rather than being sent to `/profile` to prepare for it. Kept because
|
||||
* `ProfilePage` still reads it to show what a seafarer registration will need.
|
||||
*/
|
||||
export const SEAFARER_PROFILE_REQUIREMENT: ProfileRequirement = {
|
||||
fields: [
|
||||
@@ -42,35 +32,26 @@ export const SEAFARER_PROFILE_REQUIREMENT: ProfileRequirement = {
|
||||
const REGISTRATION_TYPE_KEY = 'SEAFARER_REGISTRATION';
|
||||
|
||||
/**
|
||||
* Sends an applicant with an incomplete profile to `/profile` before they can
|
||||
* reach seafarer registration. Wraps `/seafarer-registration` directly and
|
||||
* `/licensing/:typeCode/apply` when `typeCode` is the seafarer type — the
|
||||
* latter is the shared wizard route every licence type renders through, so
|
||||
* without it the gate is a decoration a deep link skips.
|
||||
* Guards the seafarer wizard against the one case it cannot serve: an
|
||||
* applicant who already holds a seafarer number.
|
||||
*
|
||||
* Fires before the wizard starts, not mid-application, so nothing is lost —
|
||||
* unlike the case `ProfileRequirementGate`'s doc comment warns against
|
||||
* (mid-flow redirects on the old, deleted setup wizard).
|
||||
* It deliberately does *not* gate on profile completeness any more. Selecting
|
||||
* Seafarer Registration now opens the wizard, and the Identity Details step
|
||||
* collects name, gender, DOB, marital status, nationality and national ID
|
||||
* itself — an empty profile is a thing the wizard fills, not a reason to be
|
||||
* sent away from it. Those answers reach the profile when a reviewer approves
|
||||
* the registration (`CompletionEffectService.registerSeafarer`).
|
||||
*
|
||||
* Wraps `/seafarer-registration` directly and `/licensing/:typeCode/apply`
|
||||
* when `typeCode` is the seafarer type — the latter is the shared wizard route
|
||||
* every licence type renders through, so without it a deep link skips this.
|
||||
*/
|
||||
export function RequireSeafarerProfile({ children }: { children: React.ReactNode }) {
|
||||
const { t } = useTranslation();
|
||||
const { typeCode } = useParams();
|
||||
const { pathname } = useLocation();
|
||||
const { isLoading, isFetching, error, gapsFor, profile } = useCurrentProfile();
|
||||
const { isLoading, error, profile } = useCurrentProfile();
|
||||
|
||||
// Shared wizard route — only the seafarer type is gated here.
|
||||
// Shared wizard route — only the seafarer type is checked here.
|
||||
const gated = !typeCode || typeCode === REGISTRATION_TYPE_KEY;
|
||||
const gaps = gated ? gapsFor(SEAFARER_PROFILE_REQUIREMENT) : [];
|
||||
const redirecting = gated && !isLoading && !error && !isFetching && gaps.length > 0;
|
||||
|
||||
useEffect(() => {
|
||||
if (!redirecting) return;
|
||||
const fields = gaps.map((field) => t(`profileFields.${field}`, field)).join(', ');
|
||||
notify.info(t('profileGate.seafarerRedirect', { fields }));
|
||||
// Fire once per redirect, not on every render while gaps/gapsFor are
|
||||
// recreated — the toast content is captured at the moment it fires.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [redirecting, pathname]);
|
||||
|
||||
if (!gated) return <>{children}</>;
|
||||
|
||||
@@ -91,24 +72,10 @@ export function RequireSeafarerProfile({ children }: { children: React.ReactNode
|
||||
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.
|
||||
// A failed lookup must not lock anyone out — an unreadable profile says
|
||||
// nothing about whether this applicant is already registered, and the
|
||||
// server refuses a duplicate registration regardless.
|
||||
if (error) return <>{children}</>;
|
||||
|
||||
if (gaps.length === 0) return <>{children}</>;
|
||||
|
||||
// Gaps while a save is still landing are not an answer yet. Saving a
|
||||
// profile tab invalidates this query and the applicant may already be
|
||||
// headed back here in the same tick — deciding on the pre-save cache would
|
||||
// bounce them off the screen they just finished.
|
||||
if (isFetching) {
|
||||
return (
|
||||
<Center h={200}>
|
||||
<Loader />
|
||||
</Center>
|
||||
);
|
||||
}
|
||||
|
||||
const target = PROFILE_FIELD_SECTION[gaps[0]];
|
||||
return <Navigate to={`/profile#${target}`} replace />;
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
@@ -234,8 +234,8 @@ export const am: Translations = {
|
||||
addDetails: 'እነዚህን መረጃዎች ጨምር',
|
||||
viewProfile: 'ሙሉ መገለጫ ይመልከቱ',
|
||||
seafarerReason: 'የባህረኞች ምዝገባ የሚዘጋጀው ከመገለጫዎ ነው — እነዚህ መረጃዎች ራሱ ይሞላሉ።',
|
||||
seafarerRedirect: 'የባህረኛ ምዝገባ ለማድረግ መገለጫዎን ያጠናቅቁ። የሚያስፈልጉ፡ {{fields}}',
|
||||
seafarerBanner: 'የባህረኛ ምዝገባ ለማድረግ የመገለጫ መረጃ ያስፈልጋል።',
|
||||
seafarerBanner:
|
||||
'የባህረኛ ምዝገባ እነዚህን መረጃዎች ይጠይቃል፤ ከጸደቀ በኋላም መገለጫዎን ያዘምናል። እዚህ አስቀድመው ቢሞሏቸው እዚያ እንደገና መተየብ አይኖርብዎትም።',
|
||||
},
|
||||
|
||||
profileSections: {
|
||||
|
||||
@@ -234,8 +234,8 @@ export const en = {
|
||||
viewProfile: 'View full profile',
|
||||
seafarerReason:
|
||||
'Seafarer registration is built from your profile — these details fill it in for you.',
|
||||
seafarerRedirect: 'Finish your profile to register as a seafarer. Still needed: {{fields}}',
|
||||
seafarerBanner: 'Profile details are needed for seafarer registration.',
|
||||
seafarerBanner:
|
||||
'Seafarer registration asks for these details and updates your profile once approved. Filling them in here first saves you typing them there.',
|
||||
},
|
||||
|
||||
profileSections: {
|
||||
|
||||
4
pnpm-workspace.yaml
Normal file
4
pnpm-workspace.yaml
Normal file
@@ -0,0 +1,4 @@
|
||||
allowBuilds:
|
||||
core-js: set this to true or false
|
||||
esbuild: set this to true or false
|
||||
nx: set this to true or false
|
||||
Reference in New Issue
Block a user