This commit is contained in:
Estifo77
2026-07-28 10:26:55 +03:00
parent cdec4c435b
commit 173d6eccb6
3 changed files with 202 additions and 129 deletions

View File

@@ -279,7 +279,7 @@ function ProfessionTab() {
{
header: t("configuration.name"),
cell: ({ row }) => row.original.name[locale],
enabled:true
enabled: true,
},
{
header: t("configuration.description"),
@@ -294,7 +294,7 @@ function ProfessionTab() {
cell: ({ row }) => getDeptName(row.original.departmentId),
},
{
header: "",
header: "actions",
size: 90,
cell: ({ row }) => (
<Group gap="xs">

View File

@@ -1,4 +1,4 @@
import { useEffect, useMemo, useRef, useState } from 'react';
import { useEffect, useMemo, useRef, useState } from "react";
import {
Box,
Button,
@@ -9,7 +9,7 @@ import {
Text,
Title,
rem,
} from '@mantine/core';
} from "@mantine/core";
import {
IconArrowLeft,
IconArrowRight,
@@ -18,31 +18,42 @@ import {
IconLogout2,
IconMapPin,
IconUser,
} from '@tabler/icons-react';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { useNavigate } from 'react-router-dom';
import { useApiMutation } from '@ema-platform/api';
import { notify, useErrorHandler } from '@ema-platform/ui';
import { authStorage, setUser, logout, type AuthUser } from '@ema-platform/auth';
import { useAppDispatch, useAppSelector } from '../../../store/hooks';
} from "@tabler/icons-react";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { useNavigate } from "react-router-dom";
import { useApiMutation } from "@ema-platform/api";
import { notify, useErrorHandler } from "@ema-platform/ui";
import {
authStorage,
setUser,
logout,
type AuthUser,
} from "@ema-platform/auth";
import { useAppDispatch, useAppSelector } from "../../../store/hooks";
import {
ProfileFormContent,
profileSchema,
type ProfileValues,
} from '../../profile/components/ProfileFormContent';
} from "../../profile/components/ProfileFormContent";
import {
AddressFormContent,
addressSchema,
type AddressValues,
} from '../../profile/components/AddressFormContent';
} from "../../profile/components/AddressFormContent";
const STEPS = [
{ label: 'Profile', icon: IconUser },
{ label: 'Address', icon: IconMapPin },
{ label: "Profile", icon: IconUser },
{ label: "Address", icon: IconMapPin },
];
function StepIndicator({ active, completed }: { active: number; completed: number[] }) {
function StepIndicator({
active,
completed,
}: {
active: number;
completed: number[];
}) {
return (
<Box mb={32}>
<Group gap={0} align="center" wrap="nowrap">
@@ -50,31 +61,41 @@ function StepIndicator({ active, completed }: { active: number; completed: numbe
const isDone = completed.includes(i);
const isCurrent = active === i;
return (
<Group key={i} gap={0} align="center" style={{ flex: i < STEPS.length - 1 ? 1 : 'none' }}>
<Group
key={i}
gap={0}
align="center"
style={{ flex: i < STEPS.length - 1 ? 1 : "none" }}
>
<Stack gap={4} align="center" style={{ minWidth: rem(40) }}>
<Box
style={{
width: rem(40),
height: rem(40),
borderRadius: '50%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
borderRadius: "50%",
display: "flex",
alignItems: "center",
justifyContent: "center",
backgroundColor: isDone
? 'var(--mantine-color-blue-8)'
? "var(--mantine-color-blue-8)"
: isCurrent
? 'var(--mantine-color-blue-7)'
: 'var(--mantine-color-gray-1)',
border: isCurrent ? '2.5px solid var(--mantine-color-blue-5)' : '2px solid transparent',
boxShadow: isCurrent || isDone ? '0 2px 8px rgba(34, 139, 230, 0.2)' : 'none',
? "var(--mantine-color-blue-7)"
: "var(--mantine-color-gray-1)",
border: isCurrent
? "2.5px solid var(--mantine-color-blue-5)"
: "2px solid transparent",
boxShadow:
isCurrent || isDone
? "0 2px 8px rgba(34, 139, 230, 0.2)"
: "none",
flexShrink: 0,
transition: 'all 0.2s ease',
transition: "all 0.2s ease",
}}
>
{isDone ? (
<IconCheck size={18} color="white" stroke={2.5} />
) : (
<Text fw={700} fz="sm" c={isCurrent ? 'white' : 'gray.5'}>
<Text fw={700} fz="sm" c={isCurrent ? "white" : "gray.5"}>
{i + 1}
</Text>
)}
@@ -82,8 +103,8 @@ function StepIndicator({ active, completed }: { active: number; completed: numbe
<Text
fz="xs"
fw={isCurrent ? 700 : 400}
c={isCurrent ? 'blue.7' : 'dimmed'}
style={{ whiteSpace: 'nowrap' }}
c={isCurrent ? "blue.7" : "dimmed"}
style={{ whiteSpace: "nowrap" }}
>
{step.label}
</Text>
@@ -95,8 +116,8 @@ function StepIndicator({ active, completed }: { active: number; completed: numbe
flex: 1,
height: rem(2),
backgroundColor: isDone
? 'var(--mantine-color-blue-8)'
: 'var(--mantine-color-gray-2)',
? "var(--mantine-color-blue-8)"
: "var(--mantine-color-gray-2)",
marginBottom: rem(22),
}}
/>
@@ -116,19 +137,24 @@ export function ProfileSetupPage() {
const [active, setActive] = useState(0);
const [completed, setCompleted] = useState<number[]>([]);
const [submitting, setSubmitting] = useState(false);
const [professions, setProfessions] = useState<Array<{ id: string; name: { en: string } }>>([]);
const [professions, setProfessions] = useState<
Array<{ id: string; name: { en: string } }>
>([]);
const [professionsLoading, setProfessionsLoading] = useState(true);
const [profileTrigger] = useApiMutation<{ id: string }>();
const [addressTrigger] = useApiMutation<unknown>();
const [addressTrigger] = useApiMutation<{ id: string }>();
const [meTrigger] = useApiMutation<AuthUser>();
const [fetchProfessions] = useApiMutation<{ count: number; items: Array<{ id: string; name: { en: string } }> }>();
const [fetchProfessions] = useApiMutation<{
count: number;
items: Array<{ id: string; name: { en: string } }>;
}>();
const { handleError } = useErrorHandler();
const fetched = useRef(false);
useEffect(() => {
if (fetched.current) return;
fetched.current = true;
fetchProfessions({ url: '/professions?take=100', method: 'GET' })
fetchProfessions({ url: "/professions?take=100", method: "GET" })
.unwrap()
.then((data) => setProfessions(data.items ?? []))
.catch(() => setProfessions([]))
@@ -148,36 +174,45 @@ export function ProfileSetupPage() {
return map;
}, [professions]);
const nameParts = useMemo(() => (user?.name?.en || '').trim().split(/\s+/), [user]);
const profileDefaults: ProfileValues = useMemo(() => ({
professionId: '',
firstName: nameParts[0] || '',
middleName: nameParts.length > 2 ? nameParts.slice(1, -1).join(' ') : '',
lastName: nameParts.length > 1 ? nameParts[nameParts.length - 1] : '',
gender: '',
dob: '',
pob: '',
maritalStatus: '',
}), [nameParts]);
const nameParts = useMemo(
() => (user?.name?.en || "").trim().split(/\s+/),
[user],
);
const profileDefaults: ProfileValues = useMemo(
() => ({
professionId: "",
firstName: nameParts[0] || "",
middleName: nameParts.length > 2 ? nameParts.slice(1, -1).join(" ") : "",
lastName: nameParts.length > 1 ? nameParts[nameParts.length - 1] : "",
gender: "",
dob: "",
pob: "",
maritalStatus: "",
}),
[nameParts],
);
const addressDefaults: AddressValues = useMemo(() => ({
idType: '',
idNumber: '',
nationality: '',
primaryPhoneNumber: user?.phoneNumber || '',
secondaryPhoneNumber: '',
email: user?.email || '',
regionId: '',
cityId: '',
subcityId: '',
woredaId: '',
kebeleId: '',
streetAddress: '',
postalAddress: '',
emergencyContactName: '',
emergencyContactPhone: '',
emergencyContactRelation: '',
}), [user]);
const addressDefaults: AddressValues = useMemo(
() => ({
idType: "",
idNumber: "",
nationality: "",
primaryPhoneNumber: user?.phoneNumber || "",
secondaryPhoneNumber: "",
email: user?.email || "",
regionId: "",
cityId: "",
subcityId: "",
woredaId: "",
kebeleId: "",
streetAddress: "",
postalAddress: "",
emergencyContactName: "",
emergencyContactPhone: "",
emergencyContactRelation: "",
}),
[user],
);
const {
register: profileRegister,
@@ -218,29 +253,11 @@ export function ProfileSetupPage() {
try {
const pv = profileWatch();
const av = addressWatch();
const selectedProfessionName = professionNameMap[pv.professionId] ?? '';
const selectedProfessionName = professionNameMap[pv.professionId] ?? "";
const profileResult = await profileTrigger({
url: '/profiles',
method: 'POST',
body: {
userId: user?.id,
type: 'SEAFARER',
professionId: pv.professionId,
firstName: pv.firstName,
middleName: pv.middleName,
lastName: pv.lastName,
gender: pv.gender,
dob: pv.dob,
pob: pv.pob || undefined,
maritalStatus: pv.maritalStatus,
},
}).unwrap();
authStorage.setProfileId(profileResult.id);
await addressTrigger({
url: `/addresss/profile/${profileResult.id}`,
method: 'POST',
const addressData = await addressTrigger({
url: `/addresss`,
method: "POST",
body: {
idType: av.idType,
idNumber: av.idNumber,
@@ -261,11 +278,30 @@ export function ProfileSetupPage() {
},
}).unwrap();
const me = await meTrigger({ url: '/auth/me', method: 'GET' }).unwrap();
const profileResult = await profileTrigger({
url: "/profiles",
method: "POST",
body: {
userId: user?.id,
type: "SEAFARER",
professionId: pv.professionId,
firstName: pv.firstName,
middleName: pv.middleName,
lastName: pv.lastName,
gender: pv.gender,
dob: pv.dob,
pob: pv.pob || undefined,
maritalStatus: pv.maritalStatus,
addressId: addressData.id,
},
}).unwrap();
authStorage.setProfileId(profileResult.id);
const me = await meTrigger({ url: "/auth/me", method: "GET" }).unwrap();
dispatch(setUser(me));
notify.success('Profile setup complete!');
navigate('/dashboard');
notify.success("Profile setup complete!");
navigate("/dashboard");
} catch (e) {
handleError(e);
} finally {
@@ -333,7 +369,7 @@ export function ProfileSetupPage() {
leftSection={<IconLogout2 size={16} />}
onClick={() => {
dispatch(logout());
navigate('/login');
navigate("/login");
}}
>
Sign out
@@ -349,7 +385,10 @@ export function ProfileSetupPage() {
</Button>
)}
{active < STEPS.length - 1 ? (
<Button rightSection={<IconArrowRight size={16} />} onClick={onNext}>
<Button
rightSection={<IconArrowRight size={16} />}
onClick={onNext}
>
Next Step
</Button>
) : (