mirror of
https://github.com/Tria-plc/emaui.git
synced 2026-08-26 19:12:50 +00:00
Enhance certificate designer and seafarer registration features with image support and dynamic department/rank options
This commit is contained in:
@@ -9,9 +9,14 @@ import {
|
||||
} from '@mantine/core';
|
||||
import {
|
||||
conditionHolds,
|
||||
useGetActiveDepartmentsQuery,
|
||||
useGetRanksQuery,
|
||||
useLocalized,
|
||||
type Bilingual,
|
||||
type Department,
|
||||
type FormFieldConfig,
|
||||
type FormSectionConfig,
|
||||
type Rank,
|
||||
type Vessel,
|
||||
} from '@ema-platform/api';
|
||||
import { AmharicDatePicker, CountrySelect, PhoneInput } from '@ema-platform/ui';
|
||||
@@ -82,6 +87,41 @@ export function fillFromVessel(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for a SELECT field.
|
||||
*
|
||||
* A department/rank field's seed `options` are a label cache that goes stale
|
||||
* the moment a backoffice admin adds a department or rank — the field's
|
||||
* value is already resolved server-side (profile department, or
|
||||
* `eligibility.nextRank`), correct either way, but a value missing from a
|
||||
* stale cache renders as a blank Select. Live-fetched departments/ranks take
|
||||
* over the labels for these two `source`s; the seed's own `options` still
|
||||
* cover every other SELECT unchanged.
|
||||
*/
|
||||
function selectOptions(
|
||||
field: FormFieldConfig,
|
||||
currentValue: string | undefined,
|
||||
departments: Department[] | undefined,
|
||||
ranks: Rank[],
|
||||
localized: (v?: Bilingual) => string,
|
||||
): { value: string; label: string }[] {
|
||||
if (field.source === 'profile.seafarerDepartment' && departments) {
|
||||
return departments.map((d) => ({ value: d.code, label: localized(d.name) }));
|
||||
}
|
||||
if (field.source === 'eligibility.nextRank') {
|
||||
const options = ranks.map((r) => ({ value: r.key, label: localized(r.name) }));
|
||||
// The resolved rank might not be on THIS field's ladder (rank/rankEngine,
|
||||
// proficiencyDeck/Engine share one `eligibility.nextRank` source but only
|
||||
// one is ever populated) — still show it rather than a blank Select.
|
||||
if (currentValue && !options.some((o) => o.value === currentValue)) {
|
||||
const known = ranks.find((r) => r.key === currentValue);
|
||||
options.push({ value: currentValue, label: known ? localized(known.name) : currentValue });
|
||||
}
|
||||
return options;
|
||||
}
|
||||
return (field.options ?? []).map((o) => ({ value: o.value, label: localized(o.label) }));
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders one form section from the license type's configuration.
|
||||
*
|
||||
@@ -105,6 +145,21 @@ export function ConfigDrivenSection({
|
||||
(a, b) => (a.sortOrder ?? 0) - (b.sortOrder ?? 0),
|
||||
);
|
||||
|
||||
// A department/rank SELECT ships with a hardcoded `options` label list in
|
||||
// the seed, so a department or rank added later in the backoffice has no
|
||||
// label there and would render as a blank Select even though the field's
|
||||
// value (resolved server-side) is correct. Skipped when the section has
|
||||
// neither kind of field, so most sections never pay for these two queries.
|
||||
const needsDepartmentLabels = fields.some(
|
||||
(f) => f.source === 'profile.seafarerDepartment',
|
||||
);
|
||||
const needsRankLabels = fields.some((f) => f.source === 'eligibility.nextRank');
|
||||
const { data: departments } = useGetActiveDepartmentsQuery(undefined, {
|
||||
skip: !needsDepartmentLabels,
|
||||
});
|
||||
const { data: rankRes } = useGetRanksQuery(undefined, { skip: !needsRankLabels });
|
||||
const ranks = rankRes?.items ?? [];
|
||||
|
||||
return (
|
||||
<Grid>
|
||||
{fields.map((field) => {
|
||||
@@ -183,10 +238,7 @@ export function ConfigDrivenSection({
|
||||
) : field.type === 'SELECT' ? (
|
||||
<Select
|
||||
{...common}
|
||||
data={(field.options ?? []).map((o) => ({
|
||||
value: o.value,
|
||||
label: localized(o.label),
|
||||
}))}
|
||||
data={selectOptions(field, value as string | undefined, departments, ranks, localized)}
|
||||
value={(value as string) ?? null}
|
||||
onChange={(v) => onChange(field.key, v)}
|
||||
clearable={!field.required}
|
||||
|
||||
@@ -3,6 +3,8 @@ import {
|
||||
SEAFARER_REGISTRATION_FIELD_LABELS,
|
||||
SEAFARER_REGISTRATION_SECTIONS,
|
||||
displaySeafarerAnswer,
|
||||
useGetActiveDepartmentsQuery,
|
||||
useLocalized,
|
||||
type Attachment,
|
||||
type SaveSeafarerRegistration,
|
||||
} from '@ema-platform/api';
|
||||
@@ -16,6 +18,10 @@ export function RegistrationSummary({
|
||||
answers: SaveSeafarerRegistration;
|
||||
attachments?: Attachment[];
|
||||
}) {
|
||||
const localized = useLocalized();
|
||||
const { data: departments } = useGetActiveDepartmentsQuery();
|
||||
const departmentOptions = departments?.map((d) => ({ value: d.code, label: localized(d.name) }));
|
||||
|
||||
return (
|
||||
<Stack gap="md">
|
||||
{SEAFARER_REGISTRATION_SECTIONS.map((section) => (
|
||||
@@ -35,7 +41,9 @@ export function RegistrationSummary({
|
||||
</Text>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Text size="sm">{displaySeafarerAnswer(field, answers[field])}</Text>
|
||||
<Text size="sm">
|
||||
{displaySeafarerAnswer(field, answers[field], departmentOptions)}
|
||||
</Text>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
))}
|
||||
|
||||
@@ -6,6 +6,8 @@ import {
|
||||
GENDER_OPTIONS,
|
||||
HAIR_COLOR_OPTIONS,
|
||||
MARITAL_STATUS_OPTIONS,
|
||||
useGetActiveDepartmentsQuery,
|
||||
useLocalized,
|
||||
} from '@ema-platform/api';
|
||||
import {
|
||||
DateField,
|
||||
@@ -72,8 +74,21 @@ export function IdentityDetailsStep(
|
||||
);
|
||||
}
|
||||
|
||||
/** Step 2 — Identity, Address and Physical Characteristics. */
|
||||
/**
|
||||
* Step 2 — Identity, Address and Physical Characteristics.
|
||||
*
|
||||
* The department list is backoffice-managed (see the Ranks & Departments
|
||||
* configuration tab), so this fetches the live set rather than a fixed
|
||||
* three — falling back to it only until the query resolves, so the field
|
||||
* is never an empty flash.
|
||||
*/
|
||||
export function ApplicantDetailsStep(p: StepProps) {
|
||||
const localized = useLocalized();
|
||||
const { data: departments } = useGetActiveDepartmentsQuery();
|
||||
const departmentOptions =
|
||||
departments?.map((d) => ({ value: d.code, label: localized(d.name) })) ??
|
||||
DEPARTMENT_OPTIONS;
|
||||
|
||||
return (
|
||||
<Stack gap="lg">
|
||||
<SectionTitle title="Identity" />
|
||||
@@ -94,7 +109,7 @@ export function ApplicantDetailsStep(p: StepProps) {
|
||||
name="department"
|
||||
label="Department"
|
||||
required
|
||||
options={DEPARTMENT_OPTIONS}
|
||||
options={departmentOptions}
|
||||
description="The STCW department you serve in. Determines which certificates, examinations and services apply to you."
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
Reference in New Issue
Block a user