mirror of
https://github.com/Tria-plc/emaui.git
synced 2026-08-26 19:12:50 +00:00
feat: implement person name utility functions and update related components for name handling
This commit is contained in:
@@ -45,6 +45,7 @@ import {
|
|||||||
useGetApplicationQuery,
|
useGetApplicationQuery,
|
||||||
useGetAttachmentsQuery,
|
useGetAttachmentsQuery,
|
||||||
useGetLicenseTypeRequirementsQuery,
|
useGetLicenseTypeRequirementsQuery,
|
||||||
|
useGetMyApplicationsQuery,
|
||||||
useGetMyVesselsQuery,
|
useGetMyVesselsQuery,
|
||||||
usePatchSectionMutation,
|
usePatchSectionMutation,
|
||||||
useRemoveStaffMutation,
|
useRemoveStaffMutation,
|
||||||
@@ -56,7 +57,7 @@ import {
|
|||||||
type ValidationIssue,
|
type ValidationIssue,
|
||||||
type Vessel,
|
type Vessel,
|
||||||
} from "@ema-platform/api";
|
} from "@ema-platform/api";
|
||||||
import { getCountryCode, getCountryName, ModalFooter } from "@ema-platform/ui";
|
import { getCountryCode, getCountryName, ModalFooter, splitPersonName } from "@ema-platform/ui";
|
||||||
import {
|
import {
|
||||||
LICENSE_PERMISSIONS,
|
LICENSE_PERMISSIONS,
|
||||||
PORTAL_PERMISSIONS,
|
PORTAL_PERMISSIONS,
|
||||||
@@ -103,6 +104,10 @@ export function LicenseApplicationPage() {
|
|||||||
const { data: vessels } = useGetMyVesselsQuery();
|
const { data: vessels } = useGetMyVesselsQuery();
|
||||||
const [createApplication] = useCreateApplicationMutation();
|
const [createApplication] = useCreateApplicationMutation();
|
||||||
const [appId, setAppId] = useState<string | undefined>(applicationId);
|
const [appId, setAppId] = useState<string | undefined>(applicationId);
|
||||||
|
// Only fetched to recover from the 409 below — a fresh visit never needs
|
||||||
|
// the applicant's whole application list, so this stays lazy.
|
||||||
|
const { data: myApplications, refetch: fetchMyApplications } =
|
||||||
|
useGetMyApplicationsQuery(undefined, { skip: true });
|
||||||
|
|
||||||
// Create (or resume) the draft up front, so uploads have a real owner to
|
// Create (or resume) the draft up front, so uploads have a real owner to
|
||||||
// attach to and nothing is lost if the browser is closed mid-wizard.
|
// attach to and nothing is lost if the browser is closed mid-wizard.
|
||||||
@@ -111,14 +116,30 @@ export function LicenseApplicationPage() {
|
|||||||
createApplication({ licenseType: typeCode })
|
createApplication({ licenseType: typeCode })
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.then((app) => setAppId(app.id))
|
.then((app) => setAppId(app.id))
|
||||||
.catch((err) =>
|
.catch(async (err) => {
|
||||||
|
// A one-shot registration (e.g. seafarer) already has a submitted (or
|
||||||
|
// further along) application — the backend refuses a second one
|
||||||
|
// rather than silently resuming it, unlike an unfinished DRAFT. The
|
||||||
|
// applicant's intent was still "open my registration", so find the
|
||||||
|
// existing one and load it instead of leaving the page stuck on this
|
||||||
|
// toast with nothing to fetch.
|
||||||
|
if (err?.status === 409) {
|
||||||
|
const mine = myApplications ?? (await fetchMyApplications().unwrap());
|
||||||
|
const existing = mine.items.find(
|
||||||
|
(a) => a.licenseTypeId === config.licenseType.id,
|
||||||
|
);
|
||||||
|
if (existing) {
|
||||||
|
setAppId(existing.id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
notifications.show({
|
notifications.show({
|
||||||
color: "red",
|
color: "red",
|
||||||
title: "Could not start application",
|
title: "Could not start application",
|
||||||
message: extractErrorMessage(err),
|
message: extractErrorMessage(err),
|
||||||
}),
|
});
|
||||||
);
|
});
|
||||||
}, [appId, config, createApplication, typeCode]);
|
}, [appId, config, createApplication, typeCode, myApplications, fetchMyApplications]);
|
||||||
|
|
||||||
const { data: detail, refetch } = useGetApplicationQuery(appId as string, {
|
const { data: detail, refetch } = useGetApplicationQuery(appId as string, {
|
||||||
skip: !appId,
|
skip: !appId,
|
||||||
@@ -241,7 +262,23 @@ export function LicenseApplicationPage() {
|
|||||||
// authoritative, so those keep tracking it.
|
// authoritative, so those keep tracking it.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!profile || !config) return;
|
if (!profile || !config) return;
|
||||||
const context = { user: profile.user, profile };
|
// `profile.firstName/middleName/lastName` stay blank until the applicant
|
||||||
|
// saves the Maritime Profile tab once — a fresh signup arrives here
|
||||||
|
// without ever having done that. Fall back to splitting the account's
|
||||||
|
// `name.en` (the same name signup collected) so this step still
|
||||||
|
// prefills instead of opening blank.
|
||||||
|
const nameFallback = profile.user?.name?.en
|
||||||
|
? splitPersonName(profile.user.name.en)
|
||||||
|
: null;
|
||||||
|
const context = {
|
||||||
|
user: profile.user,
|
||||||
|
profile: {
|
||||||
|
...profile,
|
||||||
|
firstName: profile.firstName || nameFallback?.firstName || "",
|
||||||
|
middleName: profile.middleName || nameFallback?.middleName || "",
|
||||||
|
lastName: profile.lastName || nameFallback?.lastName || "",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
setDraft((prev) => {
|
setDraft((prev) => {
|
||||||
let changed = false;
|
let changed = false;
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ import { useForm } from 'react-hook-form';
|
|||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { notify, PageHeader, useErrorHandler, passwordSchema as strongPasswordSchema, PasswordRequirements, getCountryCode } from '@ema-platform/ui';
|
import { notify, PageHeader, useErrorHandler, passwordSchema as strongPasswordSchema, PasswordRequirements, getCountryCode, splitPersonName, joinPersonName } from '@ema-platform/ui';
|
||||||
import { useApiMutation, useLocalized } from '@ema-platform/api';
|
import { useApiMutation, useLocalized } from '@ema-platform/api';
|
||||||
import { PORTAL_PERMISSIONS, setUser, useCurrentProfile, usePermissions } from '@ema-platform/auth';
|
import { PORTAL_PERMISSIONS, setUser, useCurrentProfile, usePermissions } from '@ema-platform/auth';
|
||||||
import { SUPPORTED_LANGUAGES, type AppLanguage } from '../../../i18n/config';
|
import { SUPPORTED_LANGUAGES, type AppLanguage } from '../../../i18n/config';
|
||||||
@@ -89,15 +89,6 @@ function getInitials(name: string, fallback: string) {
|
|||||||
return letters.toUpperCase();
|
return letters.toUpperCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
function splitProfileName(fullName: string) {
|
|
||||||
const [firstName = '', middleName = '', ...lastName] = fullName.trim().split(/\s+/);
|
|
||||||
return { firstName, middleName, lastName: lastName.join(' ') };
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatProfileName({ firstName, middleName, lastName }: Pick<ProfileValues, 'firstName' | 'middleName' | 'lastName'>) {
|
|
||||||
return [firstName, middleName, lastName].filter(Boolean).join(' ');
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeName(name: string) {
|
function normalizeName(name: string) {
|
||||||
return name.trim().replace(/\s+/g, ' ');
|
return name.trim().replace(/\s+/g, ' ');
|
||||||
}
|
}
|
||||||
@@ -205,7 +196,7 @@ export function ProfilePage() {
|
|||||||
// already holds so the form does not flash empty on a refetch.
|
// already holds so the form does not flash empty on a refetch.
|
||||||
const currentProfile = resolvedProfile ?? storedProfile;
|
const currentProfile = resolvedProfile ?? storedProfile;
|
||||||
if (currentProfile) {
|
if (currentProfile) {
|
||||||
const accountName = user?.name?.en ? splitProfileName(user.name.en) : null;
|
const accountName = user?.name?.en ? splitPersonName(user.name.en) : null;
|
||||||
setLoadedProfile({
|
setLoadedProfile({
|
||||||
professionId: currentProfile.professionId || currentProfile.profession?.id || '',
|
professionId: currentProfile.professionId || currentProfile.profession?.id || '',
|
||||||
firstName: accountName?.firstName || currentProfile.firstName || '',
|
firstName: accountName?.firstName || currentProfile.firstName || '',
|
||||||
@@ -254,7 +245,7 @@ export function ProfilePage() {
|
|||||||
nameEn: z
|
nameEn: z
|
||||||
.string()
|
.string()
|
||||||
.refine(
|
.refine(
|
||||||
(name) => Object.values(splitProfileName(name)).every(Boolean),
|
(name) => Object.values(splitPersonName(name)).every(Boolean),
|
||||||
{ message: 'Enter your first, middle, and last name' },
|
{ message: 'Enter your first, middle, and last name' },
|
||||||
),
|
),
|
||||||
nameAm: z.string().min(1, { message: t('profile.validation.nameRequired') }),
|
nameAm: z.string().min(1, { message: t('profile.validation.nameRequired') }),
|
||||||
@@ -285,7 +276,7 @@ export function ProfilePage() {
|
|||||||
|
|
||||||
setIsSavingProfile(true);
|
setIsSavingProfile(true);
|
||||||
try {
|
try {
|
||||||
const profileName = splitProfileName(values.nameEn);
|
const profileName = splitPersonName(values.nameEn);
|
||||||
const saves: Promise<unknown>[] = [
|
const saves: Promise<unknown>[] = [
|
||||||
updateTrigger({
|
updateTrigger({
|
||||||
url: '/auth/update-profile',
|
url: '/auth/update-profile',
|
||||||
@@ -362,7 +353,7 @@ export function ProfilePage() {
|
|||||||
const onSaveProfile = async (values: ProfileValues) => {
|
const onSaveProfile = async (values: ProfileValues) => {
|
||||||
if (!profileId) return;
|
if (!profileId) return;
|
||||||
|
|
||||||
const fullName = formatProfileName(values);
|
const fullName = joinPersonName(values);
|
||||||
if (user && normalizeName(fullName) !== normalizeName(user.name.en)) {
|
if (user && normalizeName(fullName) !== normalizeName(user.name.en)) {
|
||||||
notify.error('Profile name must match the name in the Personal tab.');
|
notify.error('Profile name must match the name in the Personal tab.');
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -165,11 +165,14 @@ function SeaServiceTab() {
|
|||||||
const [evidenceFor, setEvidenceFor] = useState<string | null>(null);
|
const [evidenceFor, setEvidenceFor] = useState<string | null>(null);
|
||||||
const [form, setForm] = useState(EMPTY_SEA_SERVICE);
|
const [form, setForm] = useState(EMPTY_SEA_SERVICE);
|
||||||
const [grossTonnage, setGrossTonnage] = useState<number | ''>('');
|
const [grossTonnage, setGrossTonnage] = useState<number | ''>('');
|
||||||
|
const [evidenceFile, setEvidenceFile] = useState<File | null>(null);
|
||||||
|
const [uploadingEvidence, setUploadingEvidence] = useState(false);
|
||||||
|
|
||||||
const openCreate = () => {
|
const openCreate = () => {
|
||||||
setEditing(null);
|
setEditing(null);
|
||||||
setForm(EMPTY_SEA_SERVICE);
|
setForm(EMPTY_SEA_SERVICE);
|
||||||
setGrossTonnage('');
|
setGrossTonnage('');
|
||||||
|
setEvidenceFile(null);
|
||||||
setModalOpen(true);
|
setModalOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -186,6 +189,7 @@ function SeaServiceTab() {
|
|||||||
dutiesDescription: record.dutiesDescription ?? '',
|
dutiesDescription: record.dutiesDescription ?? '',
|
||||||
});
|
});
|
||||||
setGrossTonnage(record.grossTonnage ? Number(record.grossTonnage) : '');
|
setGrossTonnage(record.grossTonnage ? Number(record.grossTonnage) : '');
|
||||||
|
setEvidenceFile(null);
|
||||||
setModalOpen(true);
|
setModalOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -204,13 +208,32 @@ function SeaServiceTab() {
|
|||||||
...(grossTonnage !== '' ? { grossTonnage: Number(grossTonnage) } : {}),
|
...(grossTonnage !== '' ? { grossTonnage: Number(grossTonnage) } : {}),
|
||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
|
let recordId = editing?.id;
|
||||||
if (editing) {
|
if (editing) {
|
||||||
await updateRecord({ id: editing.id, body }).unwrap();
|
await updateRecord({ id: editing.id, body }).unwrap();
|
||||||
notify.success('Sea-service record updated');
|
notify.success('Sea-service record updated');
|
||||||
} else {
|
} else {
|
||||||
await createRecord(body).unwrap();
|
const created = await createRecord(body).unwrap();
|
||||||
|
recordId = created.id;
|
||||||
notify.success('Sea-service record added');
|
notify.success('Sea-service record added');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (evidenceFile && recordId) {
|
||||||
|
setUploadingEvidence(true);
|
||||||
|
const result = await uploadDocument({
|
||||||
|
ownerType: 'SEA_SERVICE_RECORD',
|
||||||
|
ownerId: recordId,
|
||||||
|
documentKey: 'evidence',
|
||||||
|
file: evidenceFile,
|
||||||
|
});
|
||||||
|
setUploadingEvidence(false);
|
||||||
|
if (result.ok) {
|
||||||
|
notify.success('Evidence uploaded');
|
||||||
|
} else {
|
||||||
|
notify.error(result.error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
setModalOpen(false);
|
setModalOpen(false);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notify.error(extractErrorMessage(error, 'Could not save the record'));
|
notify.error(extractErrorMessage(error, 'Could not save the record'));
|
||||||
@@ -361,6 +384,17 @@ function SeaServiceTab() {
|
|||||||
setForm({ ...form, dutiesDescription: e.target.value })
|
setForm({ ...form, dutiesDescription: e.target.value })
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
<FileButton onChange={setEvidenceFile} accept="image/*,application/pdf">
|
||||||
|
{(props) => (
|
||||||
|
<Button
|
||||||
|
{...props}
|
||||||
|
variant="light"
|
||||||
|
leftSection={<IconFileUpload size={16} />}
|
||||||
|
>
|
||||||
|
{evidenceFile ? evidenceFile.name : 'Attach evidence (optional)'}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</FileButton>
|
||||||
<Group justify="flex-end">
|
<Group justify="flex-end">
|
||||||
<Button variant="default" onClick={() => setModalOpen(false)}>
|
<Button variant="default" onClick={() => setModalOpen(false)}>
|
||||||
Cancel
|
Cancel
|
||||||
@@ -368,7 +402,7 @@ function SeaServiceTab() {
|
|||||||
<Button
|
<Button
|
||||||
onClick={save}
|
onClick={save}
|
||||||
disabled={!valid}
|
disabled={!valid}
|
||||||
loading={creating || updating}
|
loading={creating || updating || uploadingEvidence}
|
||||||
>
|
>
|
||||||
{editing ? 'Save changes' : 'Add record'}
|
{editing ? 'Save changes' : 'Add record'}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -410,10 +444,13 @@ function MedicalTab() {
|
|||||||
const [modalOpen, setModalOpen] = useState(false);
|
const [modalOpen, setModalOpen] = useState(false);
|
||||||
const [evidenceFor, setEvidenceFor] = useState<string | null>(null);
|
const [evidenceFor, setEvidenceFor] = useState<string | null>(null);
|
||||||
const [form, setForm] = useState(EMPTY_MEDICAL);
|
const [form, setForm] = useState(EMPTY_MEDICAL);
|
||||||
|
const [evidenceFile, setEvidenceFile] = useState<File | null>(null);
|
||||||
|
const [uploadingEvidence, setUploadingEvidence] = useState(false);
|
||||||
|
|
||||||
const openCreate = () => {
|
const openCreate = () => {
|
||||||
setEditing(null);
|
setEditing(null);
|
||||||
setForm(EMPTY_MEDICAL);
|
setForm(EMPTY_MEDICAL);
|
||||||
|
setEvidenceFile(null);
|
||||||
setModalOpen(true);
|
setModalOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -427,6 +464,7 @@ function MedicalTab() {
|
|||||||
fitnessStatus: certificate.fitnessStatus,
|
fitnessStatus: certificate.fitnessStatus,
|
||||||
restrictions: certificate.restrictions ?? '',
|
restrictions: certificate.restrictions ?? '',
|
||||||
});
|
});
|
||||||
|
setEvidenceFile(null);
|
||||||
setModalOpen(true);
|
setModalOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -442,13 +480,32 @@ function MedicalTab() {
|
|||||||
...(form.restrictions ? { restrictions: form.restrictions } : {}),
|
...(form.restrictions ? { restrictions: form.restrictions } : {}),
|
||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
|
let certificateId = editing?.id;
|
||||||
if (editing) {
|
if (editing) {
|
||||||
await updateCertificate({ id: editing.id, body }).unwrap();
|
await updateCertificate({ id: editing.id, body }).unwrap();
|
||||||
notify.success('Medical certificate updated');
|
notify.success('Medical certificate updated');
|
||||||
} else {
|
} else {
|
||||||
await createCertificate(body).unwrap();
|
const created = await createCertificate(body).unwrap();
|
||||||
|
certificateId = created.id;
|
||||||
notify.success('Medical certificate added');
|
notify.success('Medical certificate added');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (evidenceFile && certificateId) {
|
||||||
|
setUploadingEvidence(true);
|
||||||
|
const result = await uploadDocument({
|
||||||
|
ownerType: 'MEDICAL_CERTIFICATE',
|
||||||
|
ownerId: certificateId,
|
||||||
|
documentKey: 'evidence',
|
||||||
|
file: evidenceFile,
|
||||||
|
});
|
||||||
|
setUploadingEvidence(false);
|
||||||
|
if (result.ok) {
|
||||||
|
notify.success('Evidence uploaded');
|
||||||
|
} else {
|
||||||
|
notify.error(result.error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
setModalOpen(false);
|
setModalOpen(false);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notify.error(extractErrorMessage(error, 'Could not save the certificate'));
|
notify.error(extractErrorMessage(error, 'Could not save the certificate'));
|
||||||
@@ -575,6 +632,17 @@ function MedicalTab() {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
<FileButton onChange={setEvidenceFile} accept="image/*,application/pdf">
|
||||||
|
{(props) => (
|
||||||
|
<Button
|
||||||
|
{...props}
|
||||||
|
variant="light"
|
||||||
|
leftSection={<IconFileUpload size={16} />}
|
||||||
|
>
|
||||||
|
{evidenceFile ? evidenceFile.name : 'Attach evidence (optional)'}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</FileButton>
|
||||||
<Group justify="flex-end">
|
<Group justify="flex-end">
|
||||||
<Button variant="default" onClick={() => setModalOpen(false)}>
|
<Button variant="default" onClick={() => setModalOpen(false)}>
|
||||||
Cancel
|
Cancel
|
||||||
@@ -582,7 +650,7 @@ function MedicalTab() {
|
|||||||
<Button
|
<Button
|
||||||
onClick={save}
|
onClick={save}
|
||||||
disabled={!valid}
|
disabled={!valid}
|
||||||
loading={creating || updating}
|
loading={creating || updating || uploadingEvidence}
|
||||||
>
|
>
|
||||||
{editing ? 'Save changes' : 'Add certificate'}
|
{editing ? 'Save changes' : 'Add certificate'}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ import { useNavigate, Link } from 'react-router-dom';
|
|||||||
import { useDispatch } from 'react-redux';
|
import { useDispatch } from 'react-redux';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useApiMutation } from '@ema-platform/api';
|
import { useApiMutation } from '@ema-platform/api';
|
||||||
import { useErrorHandler, passwordSchema, PasswordRequirements } from '@ema-platform/ui';
|
import { useErrorHandler, passwordSchema, PasswordRequirements, joinPersonName } from '@ema-platform/ui';
|
||||||
import { AuthShell } from '../components/AuthShell';
|
import { AuthShell } from '../components/AuthShell';
|
||||||
import { loginSuccess, setUser } from '../store/auth.slice';
|
import { loginSuccess, setUser } from '../store/auth.slice';
|
||||||
import type { AuthUser } from '../types/auth.types';
|
import type { AuthUser } from '../types/auth.types';
|
||||||
@@ -80,7 +80,9 @@ export function SignupPage() {
|
|||||||
username: z.string().min(3, { message: t('signup.usernameMinLength', 'Username must be at least 3 characters') }),
|
username: z.string().min(3, { message: t('signup.usernameMinLength', 'Username must be at least 3 characters') }),
|
||||||
phoneNumber: z.string().min(1, { message: t('signup.phoneRequired', 'Phone number is required') }),
|
phoneNumber: z.string().min(1, { message: t('signup.phoneRequired', 'Phone number is required') }),
|
||||||
userType: z.literal('individual'),
|
userType: z.literal('individual'),
|
||||||
nameEn: z.string().min(1, { message: t('signup.nameEnRequired', 'Name (English) is required') }),
|
firstName: z.string().min(1, { message: t('signup.firstNameRequired', 'First name is required') }),
|
||||||
|
middleName: z.string().optional(),
|
||||||
|
lastName: z.string().min(1, { message: t('signup.lastNameRequired', 'Last name is required') }),
|
||||||
nameAm: z.string().optional(),
|
nameAm: z.string().optional(),
|
||||||
password: passwordSchema(8, passwordRuleLabels),
|
password: passwordSchema(8, passwordRuleLabels),
|
||||||
confirmPassword: z.string().min(1, { message: t('signup.confirmPasswordRequired', 'Confirm your password') }),
|
confirmPassword: z.string().min(1, { message: t('signup.confirmPasswordRequired', 'Confirm your password') }),
|
||||||
@@ -109,7 +111,7 @@ export function SignupPage() {
|
|||||||
username: values.username,
|
username: values.username,
|
||||||
phoneNumber: values.phoneNumber,
|
phoneNumber: values.phoneNumber,
|
||||||
userType: values.userType,
|
userType: values.userType,
|
||||||
name: { en: values.nameEn, am: values.nameAm ?? '' },
|
name: { en: joinPersonName(values), am: values.nameAm ?? '' },
|
||||||
password: values.password,
|
password: values.password,
|
||||||
confirmPassword: values.confirmPassword,
|
confirmPassword: values.confirmPassword,
|
||||||
};
|
};
|
||||||
@@ -172,14 +174,30 @@ export function SignupPage() {
|
|||||||
|
|
||||||
<form onSubmit={handleSubmit(onSubmit)}>
|
<form onSubmit={handleSubmit(onSubmit)}>
|
||||||
<Stack gap="md">
|
<Stack gap="md">
|
||||||
<SimpleGrid cols={{ base: 1, xs: 2 }} spacing="md">
|
<SimpleGrid cols={{ base: 1, sm: 3 }} spacing="md">
|
||||||
<TextInput
|
<TextInput
|
||||||
label={t('signup.nameEnLabel', 'Name (English)')}
|
label={t('signup.firstNameLabel', 'First name')}
|
||||||
placeholder={t('signup.nameEnPlaceholder', 'Abebe Bekele')}
|
placeholder={t('signup.firstNamePlaceholder', 'Abebe')}
|
||||||
leftSection={<IconUser size={18} />}
|
leftSection={<IconUser size={18} />}
|
||||||
error={errors.nameEn?.message}
|
error={errors.firstName?.message}
|
||||||
{...register('nameEn')}
|
{...register('firstName')}
|
||||||
/>
|
/>
|
||||||
|
<TextInput
|
||||||
|
label={t('signup.middleNameLabel', 'Middle name')}
|
||||||
|
placeholder={t('signup.middleNamePlaceholder', 'Kebede')}
|
||||||
|
leftSection={<IconUser size={18} />}
|
||||||
|
error={errors.middleName?.message}
|
||||||
|
{...register('middleName')}
|
||||||
|
/>
|
||||||
|
<TextInput
|
||||||
|
label={t('signup.lastNameLabel', 'Last name')}
|
||||||
|
placeholder={t('signup.lastNamePlaceholder', 'Bekele')}
|
||||||
|
leftSection={<IconUser size={18} />}
|
||||||
|
error={errors.lastName?.message}
|
||||||
|
{...register('lastName')}
|
||||||
|
/>
|
||||||
|
</SimpleGrid>
|
||||||
|
|
||||||
<TextInput
|
<TextInput
|
||||||
label={t('signup.nameAmLabel', 'Name (Amharic)')}
|
label={t('signup.nameAmLabel', 'Name (Amharic)')}
|
||||||
placeholder={t('signup.nameAmPlaceholder', 'ስም')}
|
placeholder={t('signup.nameAmPlaceholder', 'ስም')}
|
||||||
@@ -187,7 +205,6 @@ export function SignupPage() {
|
|||||||
error={errors.nameAm?.message}
|
error={errors.nameAm?.message}
|
||||||
{...register('nameAm')}
|
{...register('nameAm')}
|
||||||
/>
|
/>
|
||||||
</SimpleGrid>
|
|
||||||
|
|
||||||
<SimpleGrid cols={{ base: 1, xs: 2 }} spacing="md">
|
<SimpleGrid cols={{ base: 1, xs: 2 }} spacing="md">
|
||||||
<TextInput
|
<TextInput
|
||||||
|
|||||||
@@ -23,3 +23,4 @@ export * from "./lib/feedback/use-error-handler";
|
|||||||
export * from "./lib/data/useServerTable";
|
export * from "./lib/data/useServerTable";
|
||||||
export * from "./lib/landing/LandingPage";
|
export * from "./lib/landing/LandingPage";
|
||||||
export * from "./lib/landing/landing-copy";
|
export * from "./lib/landing/landing-copy";
|
||||||
|
export * from "./lib/utils/person-name";
|
||||||
|
|||||||
19
libs/ui/src/lib/utils/person-name.ts
Normal file
19
libs/ui/src/lib/utils/person-name.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
/**
|
||||||
|
* Splits and joins a person's name between the account's single `name.en`
|
||||||
|
* string and the profile's separate `firstName`/`middleName`/`lastName`
|
||||||
|
* fields.
|
||||||
|
*
|
||||||
|
* The account has no first/middle/last columns of its own (that model lives
|
||||||
|
* only on the profile), so this is the one place that heuristic lives —
|
||||||
|
* reused everywhere a name crosses that boundary: the signup form joins into
|
||||||
|
* it, the Identity Details wizard step and the Profile page's Personal tab
|
||||||
|
* both split out of it.
|
||||||
|
*/
|
||||||
|
export function splitPersonName(fullName: string) {
|
||||||
|
const [firstName = '', middleName = '', ...rest] = fullName.trim().split(/\s+/);
|
||||||
|
return { firstName, middleName, lastName: rest.join(' ') };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function joinPersonName(parts: { firstName: string; middleName?: string; lastName: string }) {
|
||||||
|
return [parts.firstName, parts.middleName, parts.lastName].filter(Boolean).join(' ');
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user