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

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

View File

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

View File

@@ -1,11 +1,12 @@
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';
const initialState: AuthState = {
user: null,
token: null,
isAuthenticated: false,
currentProfile: null,
};
const authSlice = createSlice({
@@ -22,23 +23,36 @@ const authSlice = createSlice({
state.user = 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) {
state.user = null;
state.token = null;
state.isAuthenticated = false;
state.currentProfile = null;
authStorage.clear();
},
hydrateAuth(state) {
const token = authStorage.getToken();
const user = authStorage.getUser<AuthUser>();
const profile = authStorage.getProfile<CurrentProfile>();
if (token && user) {
state.token = token;
state.user = user;
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;

View File

@@ -19,6 +19,7 @@ export interface AuthState {
user: AuthUser | null;
token: string | null;
isAuthenticated: boolean;
currentProfile: CurrentProfile | null;
}
export interface LoginPayload {
@@ -26,3 +27,57 @@ export interface LoginPayload {
refreshToken: string;
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)),
getProfileId: () => localStorage.getItem(key('profile-id')) ?? undefined,
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: () => {
[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),
);
document.cookie =