mirror of
https://github.com/Tria-plc/emaui.git
synced 2026-08-26 19:12:50 +00:00
feat: refactor department to organization and move certifications to configuration tab commit
This commit is contained in:
@@ -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,
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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 },
|
||||||
|
|||||||
@@ -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 /> },
|
||||||
|
|||||||
@@ -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;
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -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(() => {
|
||||||
|
|||||||
@@ -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;
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -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 =
|
||||||
|
|||||||
Reference in New Issue
Block a user