feat: refactor department to organization and move certifications to configuration tab commit

This commit is contained in:
mengstabketemaw
2026-06-29 15:57:42 +03:00
parent 3493fbbae7
commit c46747c98e
14 changed files with 215 additions and 89 deletions

View File

@@ -1,6 +1,6 @@
import { baseApi } from '@ema-platform/api'; import { baseApi } from '@ema-platform/api';
import type { import type {
Department, Organization,
Profession, Profession,
ListResponse, ListResponse,
CreateProfessionPayload, CreateProfessionPayload,
@@ -9,8 +9,8 @@ import type {
const configurationApi = baseApi.injectEndpoints({ const configurationApi = baseApi.injectEndpoints({
endpoints: (builder) => ({ endpoints: (builder) => ({
getDepartments: builder.query<ListResponse<Department>, void>({ getOrganizations: builder.query<ListResponse<Organization>, void>({
query: () => '/departments', query: () => '/organizations',
providesTags: ['Api'], providesTags: ['Api'],
}), }),
@@ -35,11 +35,11 @@ const configurationApi = baseApi.injectEndpoints({
invalidatesTags: ['Api'], invalidatesTags: ['Api'],
}), }),
}), }),
overrideExisting: false, overrideExisting: true,
}); });
export const { export const {
useGetDepartmentsQuery, useGetOrganizationsQuery,
useGetProfessionsQuery, useGetProfessionsQuery,
useCreateProfessionMutation, useCreateProfessionMutation,
useUpdateProfessionMutation, useUpdateProfessionMutation,

View File

@@ -19,12 +19,13 @@ import {
} from '@mantine/core'; } from '@mantine/core';
import { useForm } from '@mantine/form'; import { useForm } from '@mantine/form';
import { useDisclosure } from '@mantine/hooks'; import { useDisclosure } from '@mantine/hooks';
import { IconEdit, IconTrash, IconPlus, IconBriefcase, IconMap, IconInfoCircle } from '@tabler/icons-react'; import { IconEdit, IconTrash, IconPlus, IconBriefcase, IconMap, IconCertificate, IconInfoCircle } from '@tabler/icons-react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { notify } from '@ema-platform/ui'; import { notify } from '@ema-platform/ui';
import { LocationPage } from '../../location/pages/LocationPage'; import { LocationPage } from '../../location/pages/LocationPage';
import { CertificationPage } from '../../certification/pages/CertificationPage';
import { import {
useGetDepartmentsQuery, useGetOrganizationsQuery,
useGetProfessionsQuery, useGetProfessionsQuery,
useCreateProfessionMutation, useCreateProfessionMutation,
useUpdateProfessionMutation, useUpdateProfessionMutation,
@@ -129,13 +130,13 @@ function ProfessionForm({ editingProf, deptOptions, isSubmitting, onSubmit, onCa
function ProfessionTab() { function ProfessionTab() {
const { t } = useTranslation(); const { t } = useTranslation();
const { data: deptRes } = useGetDepartmentsQuery(); const { data: deptRes } = useGetOrganizationsQuery();
const { data: profRes, isLoading, isError } = useGetProfessionsQuery(); const { data: profRes, isLoading, isError } = useGetProfessionsQuery();
const [createProfession, { isLoading: isCreating }] = useCreateProfessionMutation(); const [createProfession, { isLoading: isCreating }] = useCreateProfessionMutation();
const [updateProfession, { isLoading: isUpdating }] = useUpdateProfessionMutation(); const [updateProfession, { isLoading: isUpdating }] = useUpdateProfessionMutation();
const [deleteProfession] = useDeleteProfessionMutation(); const [deleteProfession] = useDeleteProfessionMutation();
const departments = deptRes?.items ?? []; const departments = Array.isArray(deptRes) ? deptRes : (deptRes?.items ?? []);
const professions = profRes?.items ?? []; const professions = profRes?.items ?? [];
const [editingProf, setEditingProf] = useState<Profession | null>(null); const [editingProf, setEditingProf] = useState<Profession | null>(null);
@@ -143,9 +144,9 @@ function ProfessionTab() {
const [deleteTarget, setDeleteTarget] = useState<Profession | null>(null); const [deleteTarget, setDeleteTarget] = useState<Profession | null>(null);
const [deleteOpened, { open: openDelete, close: closeDelete }] = useDisclosure(false); const [deleteOpened, { open: openDelete, close: closeDelete }] = useDisclosure(false);
const deptOptions = departments.filter((d) => d.isActive).map((d) => ({ const deptOptions = departments.filter((d) => d?.status?.toLowerCase() === 'active').map((d) => ({
value: d.id, value: d.id,
label: d.name.en, label: d.name?.en ?? d.name ?? '',
})); }));
const resetProfForm = useCallback(() => { const resetProfForm = useCallback(() => {
@@ -312,6 +313,9 @@ export function ConfigurationPage() {
<Tabs.Tab value="locations" leftSection={<IconMap size={16} />}> <Tabs.Tab value="locations" leftSection={<IconMap size={16} />}>
{t('location.title')} {t('location.title')}
</Tabs.Tab> </Tabs.Tab>
<Tabs.Tab value="certifications" leftSection={<IconCertificate size={16} />}>
Certifications
</Tabs.Tab>
</Tabs.List> </Tabs.List>
<Tabs.Panel value="professions" pt="md"> <Tabs.Panel value="professions" pt="md">
@@ -321,6 +325,10 @@ export function ConfigurationPage() {
<Tabs.Panel value="locations" pt="md"> <Tabs.Panel value="locations" pt="md">
<LocationPage /> <LocationPage />
</Tabs.Panel> </Tabs.Panel>
<Tabs.Panel value="certifications" pt="md">
<CertificationPage />
</Tabs.Panel>
</Tabs> </Tabs>
</Stack> </Stack>
); );

View File

@@ -3,11 +3,15 @@ export interface NamePair {
am: string; am: string;
} }
export interface Department { export interface Organization {
id: string; id: string;
name: NamePair; name: NamePair;
description: NamePair; key: string;
isActive: boolean; isSuperAdmin: boolean;
isGovernmentOrganization: boolean;
status: string;
parentId: string | null;
organizationTypeId: string;
createdAt: string; createdAt: string;
updatedAt: string; updatedAt: string;
} }
@@ -15,7 +19,7 @@ export interface Department {
export interface Profession { export interface Profession {
id: string; id: string;
departmentId: string; departmentId: string;
department?: Department; department?: Organization;
name: NamePair; name: NamePair;
description: NamePair; description: NamePair;
isActive: boolean; isActive: boolean;

View File

@@ -20,7 +20,6 @@ import {
IconUser, IconUser,
IconUsers, IconUsers,
IconUserShield, IconUserShield,
IconCertificate,
IconQuestionMark, IconQuestionMark,
IconClipboardList, IconClipboardList,
IconReport, IconReport,
@@ -41,7 +40,6 @@ const NAV_ITEMS: NavItem[] = [
{ to: '/analytics', label: 'Analytics', icon: IconChartBar }, { to: '/analytics', label: 'Analytics', icon: IconChartBar },
{ to: '/medical-verification', label: 'Medical Verification', icon: IconHeart }, { to: '/medical-verification', label: 'Medical Verification', icon: IconHeart },
{ to: '/locations', label: 'Locations', icon: IconMap }, { to: '/locations', label: 'Locations', icon: IconMap },
{ to: '/certifications', label: 'Certifications', icon: IconCertificate },
{ to: '/questions', label: 'Questions', icon: IconQuestionMark }, { to: '/questions', label: 'Questions', icon: IconQuestionMark },
{ to: '/exams', label: 'Examinations', icon: IconClipboardList }, { to: '/exams', label: 'Examinations', icon: IconClipboardList },
{ to: '/exam-results', label: 'Exam Results', icon: IconReport }, { to: '/exam-results', label: 'Exam Results', icon: IconReport },

View File

@@ -26,7 +26,6 @@ import { MedicalVerificationPage } from '../features/medical-verification/pages/
import { PaymentConfigPage } from '../features/payment-config/pages/PaymentConfigPage'; import { PaymentConfigPage } from '../features/payment-config/pages/PaymentConfigPage';
import { SeafarerRegistryPage } from '../features/seafarer-registry/pages/SeafarerRegistryPage'; import { SeafarerRegistryPage } from '../features/seafarer-registry/pages/SeafarerRegistryPage';
import { SeamanBookQueuePage } from '../features/seaman-book-queue/pages/SeamanBookQueuePage'; import { SeamanBookQueuePage } from '../features/seaman-book-queue/pages/SeamanBookQueuePage';
import { CertificationPage } from '../features/certification/pages/CertificationPage';
import { QuestionPage } from '../features/question/pages/QuestionPage'; import { QuestionPage } from '../features/question/pages/QuestionPage';
import { ExamPage } from '../features/exam/pages/ExamPage'; import { ExamPage } from '../features/exam/pages/ExamPage';
import { ExamDetailPage } from '../features/exam/pages/ExamDetailPage'; import { ExamDetailPage } from '../features/exam/pages/ExamDetailPage';
@@ -65,7 +64,6 @@ const router = createBrowserRouter([
{ path: 'payment-config', element: <PaymentConfigPage /> }, { path: 'payment-config', element: <PaymentConfigPage /> },
{ path: 'seafarer-registry', element: <SeafarerRegistryPage /> }, { path: 'seafarer-registry', element: <SeafarerRegistryPage /> },
{ path: 'seaman-book-queue', element: <SeamanBookQueuePage /> }, { path: 'seaman-book-queue', element: <SeamanBookQueuePage /> },
{ path: 'certifications', element: <CertificationPage /> },
{ path: 'questions', element: <QuestionPage /> }, { path: 'questions', element: <QuestionPage /> },
{ path: 'exams', element: <ExamPage /> }, { path: 'exams', element: <ExamPage /> },
{ path: 'exams/:id', element: <ExamDetailPage /> }, { path: 'exams/:id', element: <ExamDetailPage /> },

View File

@@ -8,7 +8,7 @@ import {
refreshAccessToken, refreshAccessToken,
logout, logout,
} from '@ema-platform/auth'; } from '@ema-platform/auth';
import type { AuthUser } from '@ema-platform/auth'; import type { AuthUser, CurrentProfile } from '@ema-platform/auth';
import { preferencesReducer } from './preferences.slice'; import { preferencesReducer } from './preferences.slice';
configureAuthStorage('ema-backoffice'); configureAuthStorage('ema-backoffice');
@@ -16,8 +16,9 @@ configureAuthStorage('ema-backoffice');
const preloadedAuth = (() => { const preloadedAuth = (() => {
const token = authStorage.getToken(); const token = authStorage.getToken();
const user = authStorage.getUser<AuthUser>(); const user = authStorage.getUser<AuthUser>();
const profile = authStorage.getProfile<CurrentProfile>();
if (token && user) { if (token && user) {
return { token, user, isAuthenticated: true }; return { token, user, isAuthenticated: true, currentProfile: profile ?? null };
} }
return undefined; return undefined;
})(); })();

View File

@@ -45,7 +45,8 @@ import { z } from 'zod';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { notify, PageHeader } from '@ema-platform/ui'; import { notify, PageHeader } from '@ema-platform/ui';
import { useApiMutation } from '@ema-platform/api'; import { useApiMutation } from '@ema-platform/api';
import { authStorage, setUser } from '@ema-platform/auth'; import { authStorage, setUser, setCurrentProfile } from '@ema-platform/auth';
import type { CurrentProfile } from '@ema-platform/auth';
import { SUPPORTED_LANGUAGES, type AppLanguage } from '../../../i18n/config'; import { SUPPORTED_LANGUAGES, type AppLanguage } from '../../../i18n/config';
import { useAppDispatch, useAppSelector } from '../../../store/hooks'; import { useAppDispatch, useAppSelector } from '../../../store/hooks';
import type { AuthUser } from '@ema-platform/auth'; import type { AuthUser } from '@ema-platform/auth';
@@ -83,6 +84,7 @@ export function ProfilePage() {
const { t, i18n } = useTranslation(); const { t, i18n } = useTranslation();
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const user = useAppSelector((state) => state.auth.user); const user = useAppSelector((state) => state.auth.user);
const currentProfile = useAppSelector((state) => state.auth.currentProfile);
const { colorScheme, setColorScheme } = useMantineColorScheme(); const { colorScheme, setColorScheme } = useMantineColorScheme();
const [updateTrigger] = useApiMutation<AuthUser>(); const [updateTrigger] = useApiMutation<AuthUser>();
@@ -124,8 +126,8 @@ export function ProfilePage() {
return map; return map;
}, [professions]); }, [professions]);
// ---- Fetch profile data (for Profile & Address tabs) ---- // ---- Profile data (from stored currentProfile) ----
const [fetchProfile] = useApiMutation<Record<string, any>>(); const [fetchProfile] = useApiMutation<{ total: number; items: CurrentProfile[] }>();
const [updateProfile] = useApiMutation<unknown>(); const [updateProfile] = useApiMutation<unknown>();
const [updateAddress] = useApiMutation<unknown>(); const [updateAddress] = useApiMutation<unknown>();
@@ -134,51 +136,67 @@ export function ProfilePage() {
const [profileId, setProfileId] = useState<string | null>(null); const [profileId, setProfileId] = useState<string | null>(null);
const [addressId, setAddressId] = useState<string | null>(null); const [addressId, setAddressId] = useState<string | null>(null);
const [dataLoading, setDataLoading] = useState(true); const [dataLoading, setDataLoading] = useState(true);
const profileFetched = useRef(false);
useEffect(() => { useEffect(() => {
if (!user) return; if (currentProfile) {
fetchProfile({ url: `/profiles/by-user/${user.id}`, method: 'GET' }) setProfileId(currentProfile.id);
.unwrap() setLoadedProfile({
.then((data) => { professionId: currentProfile.professionId || currentProfile.profession?.id || '',
setProfileId(data.id); firstName: currentProfile.firstName || '',
setLoadedProfile({ middleName: currentProfile.middleName || '',
professionId: data.professionId || data.profession?.id || '', lastName: currentProfile.lastName || '',
firstName: data.firstName || '', gender: currentProfile.gender || '',
middleName: data.middleName || '', dob: currentProfile.dob ? currentProfile.dob.split('T')[0] : '',
lastName: data.lastName || '', pob: currentProfile.pob || '',
gender: data.gender || '', maritalStatus: currentProfile.maritalStatus || '',
dob: data.dob ? data.dob.split('T')[0] : '',
pob: data.pob || '',
maritalStatus: data.maritalStatus || '',
});
if (data.address) {
setAddressId(data.address.id);
setLoadedAddress({
idType: data.address.idType || '',
idNumber: data.address.idNumber || '',
nationality: data.address.nationality || '',
primaryPhoneNumber: data.address.primaryPhoneNumber || '',
secondaryPhoneNumber: data.address.secondaryPhoneNumber || '',
email: data.address.email || '',
regionId: data.address.regionId || '',
cityId: data.address.cityId || '',
subcityId: data.address.subCityId || '',
woredaId: data.address.woredaId || '',
kebeleId: data.address.kebeleId || '',
streetAddress: data.address.streetAddress || '',
postalAddress: data.address.postalAddress || '',
emergencyContactName: data.address.emergencyContactName || '',
emergencyContactPhone: data.address.emergencyContactPhone || '',
emergencyContactRelation: data.address.emergencycontactRelation || '',
});
}
setDataLoading(false);
})
.catch(() => {
setDataLoading(false);
}); });
}, [user, fetchProfile]);
if (currentProfile.address) {
setAddressId(currentProfile.address.id);
setLoadedAddress({
idType: currentProfile.address.idType || '',
idNumber: currentProfile.address.idNumber || '',
nationality: currentProfile.address.nationality || '',
primaryPhoneNumber: currentProfile.address.primaryPhoneNumber || '',
secondaryPhoneNumber: currentProfile.address.secondaryPhoneNumber || '',
email: currentProfile.address.email || '',
regionId: currentProfile.address.regionId || '',
cityId: currentProfile.address.cityId || '',
subcityId: currentProfile.address.subCityId || '',
woredaId: currentProfile.address.woredaId || '',
kebeleId: currentProfile.address.kebeleId || '',
streetAddress: currentProfile.address.streetAddress || '',
postalAddress: currentProfile.address.postalAddress || '',
emergencyContactName: currentProfile.address.emergencyContactName || '',
emergencyContactPhone: currentProfile.address.emergencyContactPhone || '',
emergencyContactRelation: currentProfile.address.emergencycontactRelation || '',
});
}
setDataLoading(false);
} else if (user && !profileFetched.current) {
profileFetched.current = true;
const profileId = authStorage.getProfileId();
if (profileId) {
const q = `w=user_id:=:${user.id}&i=user,address,profession`;
fetchProfile({ url: `/profiles?q=${encodeURIComponent(q)}`, method: 'GET' })
.unwrap()
.then((result) => {
if (result.total > 0 && result.items.length > 0) {
const profile = result.items[0];
dispatch(setCurrentProfile(profile));
} else {
setDataLoading(false);
}
})
.catch(() => setDataLoading(false));
} else {
setDataLoading(false);
}
} else {
setDataLoading(false);
}
}, [currentProfile, user, fetchProfile, dispatch]);
// Load the latest user from the server on mount // Load the latest user from the server on mount
useEffect(() => { useEffect(() => {

View File

@@ -8,15 +8,16 @@ import {
refreshAccessToken, refreshAccessToken,
logout, logout,
} from '@ema-platform/auth'; } from '@ema-platform/auth';
import type { AuthUser } from '@ema-platform/auth'; import type { AuthUser, CurrentProfile } from '@ema-platform/auth';
configureAuthStorage('ema-portal'); configureAuthStorage('ema-portal');
const preloadedAuth = (() => { const preloadedAuth = (() => {
const token = authStorage.getToken(); const token = authStorage.getToken();
const user = authStorage.getUser<AuthUser>(); const user = authStorage.getUser<AuthUser>();
const profile = authStorage.getProfile<CurrentProfile>();
if (token && user) { if (token && user) {
return { token, user, isAuthenticated: true }; return { token, user, isAuthenticated: true, currentProfile: profile ?? null };
} }
return undefined; return undefined;
})(); })();

View File

@@ -6,8 +6,8 @@ export { LoginPage } from './lib/pages/LoginPage';
export { SignupPage } from './lib/pages/SignupPage'; export { SignupPage } from './lib/pages/SignupPage';
export { ForgotPasswordPage } from './lib/pages/ForgotPasswordPage'; export { ForgotPasswordPage } from './lib/pages/ForgotPasswordPage';
export { OTPVerificationPage } from './lib/pages/OTPVerificationPage'; export { OTPVerificationPage } from './lib/pages/OTPVerificationPage';
export { authReducer, loginSuccess, setUser, logout, hydrateAuth } from './lib/store/auth.slice'; export { authReducer, loginSuccess, setUser, setCurrentProfile, clearCurrentProfile, logout, hydrateAuth } from './lib/store/auth.slice';
export { signupReducer, setSignupData, setSignupStep, resetSignup } from './lib/store/signup.slice'; export { signupReducer, setSignupData, setSignupStep, resetSignup } from './lib/store/signup.slice';
export { configureAuthStorage, authStorage } from './lib/utils/auth-storage'; export { configureAuthStorage, authStorage } from './lib/utils/auth-storage';
export { refreshAccessToken } from './lib/utils/refresh-token'; export { refreshAccessToken } from './lib/utils/refresh-token';
export type { AuthUser, AuthState, LoginPayload } from './lib/types/auth.types'; export type { AuthUser, AuthState, LoginPayload, CurrentProfile, CurrentProfileAddress, CurrentProfileProfession } from './lib/types/auth.types';

View File

@@ -26,8 +26,8 @@ import { useDispatch } from 'react-redux';
import { useApiMutation } from '@ema-platform/api'; import { useApiMutation } from '@ema-platform/api';
import { notify } from '@ema-platform/ui'; import { notify } from '@ema-platform/ui';
import { AuthShell } from '../components/AuthShell'; import { AuthShell } from '../components/AuthShell';
import { loginSuccess, setUser } from '../store/auth.slice'; import { loginSuccess, setUser, setCurrentProfile } from '../store/auth.slice';
import type { LoginPayload, AuthUser } from '../types/auth.types'; import type { LoginPayload, AuthUser, CurrentProfile } from '../types/auth.types';
import { useAuthConfig } from '../AuthConfig'; import { useAuthConfig } from '../AuthConfig';
import { authStorage } from '../utils/auth-storage'; import { authStorage } from '../utils/auth-storage';
@@ -48,7 +48,7 @@ export function LoginPage() {
const [serverError, setServerError] = useState<string | null>(null); const [serverError, setServerError] = useState<string | null>(null);
const [loginTrigger] = useApiMutation<LoginPayload>(); const [loginTrigger] = useApiMutation<LoginPayload>();
const [meTrigger] = useApiMutation<AuthUser>(); const [meTrigger] = useApiMutation<AuthUser>();
const [profileCheckTrigger] = useApiMutation<{ id: string }>(); const [profileCheckTrigger] = useApiMutation<{ total: number; items: CurrentProfile[] }>();
const { const {
register, register,
@@ -76,12 +76,17 @@ export function LoginPage() {
let hasProfile = false; let hasProfile = false;
try { try {
const profile = await profileCheckTrigger({ const q = `w=user_id:=:${me.id}&i=user,address,profession`;
url: `/profiles/by-user/${me.id}`, const result = await profileCheckTrigger({
url: `/profiles?q=${encodeURIComponent(q)}`,
method: 'GET', method: 'GET',
}).unwrap(); }).unwrap();
authStorage.setProfileId(profile.id); if (result.total > 0 && result.items.length > 0) {
hasProfile = true; const profile = result.items[0];
authStorage.setProfileId(profile.id);
dispatch(setCurrentProfile(profile));
hasProfile = true;
}
} catch { } catch {
// profile not found — redirect to setup // profile not found — redirect to setup
} }

View File

@@ -1,5 +1,6 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { import {
Alert,
Anchor, Anchor,
Button, Button,
Center, Center,
@@ -46,6 +47,7 @@ export function OTPVerificationPage() {
const [verifyTrigger, { isLoading: loading }] = useApiMutation(); const [verifyTrigger, { isLoading: loading }] = useApiMutation();
const [resendTrigger, { isLoading: resending }] = useApiMutation(); const [resendTrigger, { isLoading: resending }] = useApiMutation();
const [secondsLeft, setSecondsLeft] = useState(RESEND_SECONDS); const [secondsLeft, setSecondsLeft] = useState(RESEND_SECONDS);
const [serverError, setServerError] = useState<string | null>(null);
const { const {
control, control,
@@ -72,8 +74,11 @@ export function OTPVerificationPage() {
notify.success('Phone number verified successfully'); notify.success('Phone number verified successfully');
navigate(needsProfile ? '/profile-setup' : loginRedirectPath); navigate(needsProfile ? '/profile-setup' : loginRedirectPath);
} catch (err) { } catch (err: unknown) {
const msg = err instanceof Error ? err.message : 'Something went wrong'; const msg =
(err as { data?: { message?: string } })?.data?.message ??
(err instanceof Error ? err.message : 'Something went wrong');
setServerError(msg);
notify.error(msg); notify.error(msg);
} }
}; };
@@ -82,15 +87,19 @@ export function OTPVerificationPage() {
if (secondsLeft > 0 || resending) return; if (secondsLeft > 0 || resending) return;
try { try {
await resendTrigger({ await resendTrigger({
url: '/auth/resend-otp', url: '/auth/generate-verification-code',
method: 'POST', method: 'PATCH',
body: { email }, body: { email, phoneNumber, type: 'verify-phone-number' },
}).unwrap(); }).unwrap();
notify.success('Verification code resent to your email'); notify.success('Verification code resent to your email');
setSecondsLeft(RESEND_SECONDS); setSecondsLeft(RESEND_SECONDS);
} catch (err) { setServerError(null);
const msg = err instanceof Error ? err.message : 'Something went wrong'; } catch (err: unknown) {
const msg =
(err as { data?: { message?: string } })?.data?.message ??
(err instanceof Error ? err.message : 'Something went wrong');
setServerError(msg);
notify.error(msg); notify.error(msg);
} }
}; };
@@ -118,6 +127,12 @@ export function OTPVerificationPage() {
</Text> </Text>
</div> </div>
{serverError && (
<Alert variant="light" color="red" withCloseButton onClose={() => setServerError(null)}>
{serverError}
</Alert>
)}
<form onSubmit={handleSubmit(onSubmit)}> <form onSubmit={handleSubmit(onSubmit)}>
<Stack gap="md"> <Stack gap="md">
<Controller <Controller
@@ -127,8 +142,8 @@ export function OTPVerificationPage() {
<Stack gap={6} align="center"> <Stack gap={6} align="center">
<PinInput <PinInput
length={CODE_LENGTH} length={CODE_LENGTH}
type="number" type="text"
inputMode="numeric" inputMode="text"
oneTimeCode oneTimeCode
size="md" size="md"
gap="sm" gap="sm"

View File

@@ -1,11 +1,12 @@
import { createSlice, type PayloadAction } from '@reduxjs/toolkit'; import { createSlice, type PayloadAction } from '@reduxjs/toolkit';
import type { AuthState, AuthUser, LoginPayload } from '../types/auth.types'; import type { AuthState, AuthUser, CurrentProfile, LoginPayload } from '../types/auth.types';
import { authStorage } from '../utils/auth-storage'; import { authStorage } from '../utils/auth-storage';
const initialState: AuthState = { const initialState: AuthState = {
user: null, user: null,
token: null, token: null,
isAuthenticated: false, isAuthenticated: false,
currentProfile: null,
}; };
const authSlice = createSlice({ const authSlice = createSlice({
@@ -22,23 +23,36 @@ const authSlice = createSlice({
state.user = action.payload; state.user = action.payload;
authStorage.setUser(action.payload); authStorage.setUser(action.payload);
}, },
setCurrentProfile(state, action: PayloadAction<CurrentProfile>) {
state.currentProfile = action.payload;
authStorage.setProfile(action.payload);
},
clearCurrentProfile(state) {
state.currentProfile = null;
authStorage.removeProfile();
},
logout(state) { logout(state) {
state.user = null; state.user = null;
state.token = null; state.token = null;
state.isAuthenticated = false; state.isAuthenticated = false;
state.currentProfile = null;
authStorage.clear(); authStorage.clear();
}, },
hydrateAuth(state) { hydrateAuth(state) {
const token = authStorage.getToken(); const token = authStorage.getToken();
const user = authStorage.getUser<AuthUser>(); const user = authStorage.getUser<AuthUser>();
const profile = authStorage.getProfile<CurrentProfile>();
if (token && user) { if (token && user) {
state.token = token; state.token = token;
state.user = user; state.user = user;
state.isAuthenticated = true; state.isAuthenticated = true;
if (profile) {
state.currentProfile = profile;
}
} }
}, },
}, },
}); });
export const { loginSuccess, setUser, logout, hydrateAuth } = authSlice.actions; export const { loginSuccess, setUser, setCurrentProfile, clearCurrentProfile, logout, hydrateAuth } = authSlice.actions;
export const authReducer = authSlice.reducer; export const authReducer = authSlice.reducer;

View File

@@ -19,6 +19,7 @@ export interface AuthState {
user: AuthUser | null; user: AuthUser | null;
token: string | null; token: string | null;
isAuthenticated: boolean; isAuthenticated: boolean;
currentProfile: CurrentProfile | null;
} }
export interface LoginPayload { export interface LoginPayload {
@@ -26,3 +27,57 @@ export interface LoginPayload {
refreshToken: string; refreshToken: string;
isPhoneNumberVerified: boolean; isPhoneNumberVerified: boolean;
} }
export interface CurrentProfileAddress {
id: string;
idType: string;
idNumber: string;
nationality: string;
regionId: string | null;
cityId: string | null;
subCityId: string | null;
woredaId: string | null;
kebeleId: string | null;
streetAddress: string | null;
houseNumber: string | null;
primaryPhoneNumber: string;
secondaryPhoneNumber: string | null;
email: string | null;
website: string | null;
postalAddress: string | null;
emergencyContactName: string | null;
emergencyContactPhone: string | null;
emergencycontactRelation: string | null;
isActive: boolean;
}
export interface CurrentProfileProfession {
id: string;
departmentId: string;
name: { en: string };
description: { en: string };
isActive: boolean;
}
export interface CurrentProfile {
id: string;
userId: string;
professionId: string;
addressId: string;
type: string;
firstName: string;
middleName: string;
lastName: string;
gender: string;
dob: string;
pob: string;
maritalStatus: string;
isComplete: boolean;
user: AuthUser;
address: CurrentProfileAddress;
profession: CurrentProfileProfession;
}
export interface CurrentProfileState {
profile: CurrentProfile | null;
}

View File

@@ -23,8 +23,17 @@ export const authStorage = {
setUser: <T>(u: T) => localStorage.setItem(key('auth-user'), JSON.stringify(u)), setUser: <T>(u: T) => localStorage.setItem(key('auth-user'), JSON.stringify(u)),
getProfileId: () => localStorage.getItem(key('profile-id')) ?? undefined, getProfileId: () => localStorage.getItem(key('profile-id')) ?? undefined,
setProfileId: (id: string) => localStorage.setItem(key('profile-id'), id), setProfileId: (id: string) => localStorage.setItem(key('profile-id'), id),
getProfile: <T = unknown>(): T | null => {
try {
return JSON.parse(localStorage.getItem(key('current-profile')) ?? 'null') as T | null;
} catch {
return null;
}
},
setProfile: <T>(p: T) => localStorage.setItem(key('current-profile'), JSON.stringify(p)),
removeProfile: () => localStorage.removeItem(key('current-profile')),
clear: () => { clear: () => {
[key('auth-token'), key('refresh-token'), key('auth-user'), key('profile-id')].forEach((k) => [key('auth-token'), key('refresh-token'), key('auth-user'), key('profile-id'), key('current-profile')].forEach((k) =>
localStorage.removeItem(k), localStorage.removeItem(k),
); );
document.cookie = document.cookie =