mirror of
https://github.com/Tria-plc/emaui.git
synced 2026-09-07 08:25:45 +00:00
feat: add date bounds to DateField and improve validation flow in registration wizard
This commit is contained in:
@@ -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 (
|
return (
|
||||||
<Col span={p.span}>
|
<Col span={p.span}>
|
||||||
<AmharicDatePicker
|
<AmharicDatePicker
|
||||||
@@ -70,6 +70,8 @@ export function DateField(p: FieldProps) {
|
|||||||
disabled={p.disabled}
|
disabled={p.disabled}
|
||||||
required={p.required}
|
required={p.required}
|
||||||
dateFormat="date"
|
dateFormat="date"
|
||||||
|
minDate={p.minDate}
|
||||||
|
maxDate={p.maxDate}
|
||||||
value={(p.form[p.name] as string) ?? ''}
|
value={(p.form[p.name] as string) ?? ''}
|
||||||
onChange={(v) => p.set(p.name, v)}
|
onChange={(v) => p.set(p.name, v)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -220,6 +220,10 @@ export function EmergencyContactStep(p: StepProps) {
|
|||||||
name="medicalIssueDate"
|
name="medicalIssueDate"
|
||||||
label="Issue Date"
|
label="Issue Date"
|
||||||
required
|
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."
|
description="Cannot be a future date. Validity is calculated from this: two years, or one year if you are under 18."
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|||||||
@@ -80,6 +80,11 @@ function answersOf(registration: SeafarerRegistration): SaveSeafarerRegistration
|
|||||||
return Object.fromEntries(ANSWER_KEYS.map((k) => [k, registration[k]])) as 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 {
|
function blank(value: unknown): boolean {
|
||||||
return value === null || value === undefined || value === '' || value === false;
|
return value === null || value === undefined || value === '' || value === false;
|
||||||
}
|
}
|
||||||
@@ -258,6 +263,10 @@ export function SeafarerRegistrationPage() {
|
|||||||
|
|
||||||
function set(key: AnswerKey, value: unknown) {
|
function set(key: AnswerKey, value: unknown) {
|
||||||
setForm((prev) => ({ ...prev, [key]: value }));
|
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) => {
|
setErrors((prev) => {
|
||||||
if (!prev[key]) return prev;
|
if (!prev[key]) return prev;
|
||||||
const next = { ...prev };
|
const next = { ...prev };
|
||||||
@@ -280,6 +289,9 @@ export function SeafarerRegistrationPage() {
|
|||||||
found.weightKg = `Enter a weight between ${weightKg.min} and ${weightKg.max} kg.`;
|
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);
|
setErrors(found);
|
||||||
const missingKeys = Object.keys(found) as AnswerKey[];
|
const missingKeys = Object.keys(found) as AnswerKey[];
|
||||||
if (missingKeys.length) {
|
if (missingKeys.length) {
|
||||||
@@ -323,19 +335,23 @@ export function SeafarerRegistrationPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function goToStep(target: number) {
|
async function goToStep(target: number) {
|
||||||
if (target <= active) {
|
// Saved before anything can turn the navigation around. `form` is the only
|
||||||
setActive(target);
|
// copy of what was typed, so validating first — as this used to — threw the
|
||||||
return;
|
// 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
|
||||||
// Going forward validates every step passed over, so a jump cannot skip a
|
// takes any subset of the answers, so persisting an incomplete one is safe.
|
||||||
// required field; the walk stops on the first step that fails.
|
const saved = await saveAnswers();
|
||||||
for (let step = active; step < target; step++) {
|
if (target > active) {
|
||||||
if (!readOnly && !validateStep(step)) {
|
if (!saved) return;
|
||||||
setActive(step);
|
// Going forward validates every step passed over, so a jump cannot skip a
|
||||||
return;
|
// 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({});
|
setErrors({});
|
||||||
setActive(target);
|
setActive(target);
|
||||||
}
|
}
|
||||||
@@ -343,8 +359,8 @@ export function SeafarerRegistrationPage() {
|
|||||||
async function handleSubmit() {
|
async function handleSubmit() {
|
||||||
if (!registration) return;
|
if (!registration) return;
|
||||||
setIssues([]);
|
setIssues([]);
|
||||||
if (!readOnly && !validateStep(4)) return;
|
|
||||||
if (!(await saveAnswers())) return;
|
if (!(await saveAnswers())) return;
|
||||||
|
if (!readOnly && !validateStep(4)) return;
|
||||||
try {
|
try {
|
||||||
await submit(registration.id).unwrap();
|
await submit(registration.id).unwrap();
|
||||||
notifications.show({
|
notifications.show({
|
||||||
@@ -510,7 +526,7 @@ export function SeafarerRegistrationPage() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<Group justify="space-between" mt="xl">
|
<Group justify="space-between" mt="xl">
|
||||||
<Button variant="default" onClick={() => setActive((s) => Math.max(0, s - 1))} disabled={active === 0}>
|
<Button variant="default" onClick={() => goToStep(Math.max(0, active - 1))} disabled={active === 0}>
|
||||||
Back
|
Back
|
||||||
</Button>
|
</Button>
|
||||||
{active < STEPS.length - 1 ? (
|
{active < STEPS.length - 1 ? (
|
||||||
|
|||||||
Reference in New Issue
Block a user