diff --git a/apps/portal/src/app/features/seafarer-registration/components/fields.tsx b/apps/portal/src/app/features/seafarer-registration/components/fields.tsx
index 5676ec900..95a205327 100644
--- a/apps/portal/src/app/features/seafarer-registration/components/fields.tsx
+++ b/apps/portal/src/app/features/seafarer-registration/components/fields.tsx
@@ -61,7 +61,7 @@ export function SelectField(p: FieldProps & { options: { value: string; label: s
);
}
-export function DateField(p: FieldProps) {
+export function DateField(p: FieldProps & { minDate?: Date | string; maxDate?: Date | string }) {
return (
p.set(p.name, v)}
/>
diff --git a/apps/portal/src/app/features/seafarer-registration/components/steps.tsx b/apps/portal/src/app/features/seafarer-registration/components/steps.tsx
index 934244cd0..eb5abf9b8 100644
--- a/apps/portal/src/app/features/seafarer-registration/components/steps.tsx
+++ b/apps/portal/src/app/features/seafarer-registration/components/steps.tsx
@@ -220,6 +220,10 @@ export function EmergencyContactStep(p: StepProps) {
name="medicalIssueDate"
label="Issue Date"
required
+ // Bounded here rather than only at submit: the calendar is the one
+ // place the applicant can see why a day is refused, and the server's
+ // rejection otherwise only surfaces five steps later.
+ maxDate={new Date()}
description="Cannot be a future date. Validity is calculated from this: two years, or one year if you are under 18."
/>
diff --git a/apps/portal/src/app/features/seafarer-registration/pages/SeafarerRegistrationPage.tsx b/apps/portal/src/app/features/seafarer-registration/pages/SeafarerRegistrationPage.tsx
index 04ff5e01f..b324f04c8 100644
--- a/apps/portal/src/app/features/seafarer-registration/pages/SeafarerRegistrationPage.tsx
+++ b/apps/portal/src/app/features/seafarer-registration/pages/SeafarerRegistrationPage.tsx
@@ -80,6 +80,11 @@ function answersOf(registration: SeafarerRegistration): SaveSeafarerRegistration
return Object.fromEntries(ANSWER_KEYS.map((k) => [k, registration[k]])) as SaveSeafarerRegistration;
}
+/** Today as `yyyy-mm-dd` in the browser's own zone — en-CA is that format. */
+function todayDate(): string {
+ return new Date().toLocaleDateString('en-CA');
+}
+
function blank(value: unknown): boolean {
return value === null || value === undefined || value === '' || value === false;
}
@@ -258,6 +263,10 @@ export function SeafarerRegistrationPage() {
function set(key: AnswerKey, value: unknown) {
setForm((prev) => ({ ...prev, [key]: value }));
+ // The submit refusal names what was wrong at the time it was refused.
+ // Leaving it on screen while the applicant corrects it reads as the
+ // correction having been ignored.
+ if (issues.length) setIssues([]);
setErrors((prev) => {
if (!prev[key]) return prev;
const next = { ...prev };
@@ -280,6 +289,9 @@ export function SeafarerRegistrationPage() {
found.weightKg = `Enter a weight between ${weightKg.min} and ${weightKg.max} kg.`;
}
}
+ if (index === 2 && (form.medicalIssueDate ?? '').slice(0, 10) > todayDate()) {
+ found.medicalIssueDate = 'The issue date cannot be in the future.';
+ }
setErrors(found);
const missingKeys = Object.keys(found) as AnswerKey[];
if (missingKeys.length) {
@@ -323,19 +335,23 @@ export function SeafarerRegistrationPage() {
}
async function goToStep(target: number) {
- if (target <= active) {
- setActive(target);
- return;
- }
- // Going forward validates every step passed over, so a jump cannot skip a
- // required field; the walk stops on the first step that fails.
- for (let step = active; step < target; step++) {
- if (!readOnly && !validateStep(step)) {
- setActive(step);
- return;
+ // Saved before anything can turn the navigation around. `form` is the only
+ // copy of what was typed, so validating first — as this used to — threw the
+ // edit away on every blocked step and every step back: an applicant fixing
+ // a field the submit check rejected watched the correction vanish. A draft
+ // takes any subset of the answers, so persisting an incomplete one is safe.
+ const saved = await saveAnswers();
+ if (target > active) {
+ if (!saved) return;
+ // Going forward validates every step passed over, so a jump cannot skip a
+ // required field; the walk stops on the first step that fails.
+ for (let step = active; step < target; step++) {
+ if (!readOnly && !validateStep(step)) {
+ setActive(step);
+ return;
+ }
}
}
- if (!(await saveAnswers())) return;
setErrors({});
setActive(target);
}
@@ -343,8 +359,8 @@ export function SeafarerRegistrationPage() {
async function handleSubmit() {
if (!registration) return;
setIssues([]);
- if (!readOnly && !validateStep(4)) return;
if (!(await saveAnswers())) return;
+ if (!readOnly && !validateStep(4)) return;
try {
await submit(registration.id).unwrap();
notifications.show({
@@ -510,7 +526,7 @@ export function SeafarerRegistrationPage() {
)}
-