mirror of
https://github.com/Tria-plc/emaui.git
synced 2026-08-29 10:40:59 +00:00
fix: fix redirect based on profile bug
This commit is contained in:
@@ -1,7 +1,6 @@
|
||||
import { I18nextProvider } from 'react-i18next';
|
||||
import { Provider } from 'react-redux';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { AuthConfigProvider } from '@ema-platform/auth';
|
||||
import type { ReactNode } from 'react';
|
||||
import { store } from '../store';
|
||||
import { i18n } from '../i18n/config';
|
||||
@@ -17,19 +16,9 @@ export function AppProviders({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<Provider store={store}>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<AuthConfigProvider
|
||||
value={{
|
||||
appName: 'Backoffice',
|
||||
storagePrefix: 'ema-backoffice',
|
||||
loginRedirectPath: '/dashboard',
|
||||
enableSignup: false,
|
||||
enableForgotPassword: true,
|
||||
}}
|
||||
>
|
||||
<I18nextProvider i18n={i18n}>
|
||||
<MantineThemeProvider>{children}</MantineThemeProvider>
|
||||
</I18nextProvider>
|
||||
</AuthConfigProvider>
|
||||
<I18nextProvider i18n={i18n}>
|
||||
<MantineThemeProvider>{children}</MantineThemeProvider>
|
||||
</I18nextProvider>
|
||||
</QueryClientProvider>
|
||||
</Provider>
|
||||
);
|
||||
|
||||
@@ -41,8 +41,8 @@ const router = createBrowserRouter([
|
||||
{
|
||||
element: <AuthLayout />,
|
||||
children: [
|
||||
{ path: '/login', element: <LoginPage /> },
|
||||
{ path: '/forgot-password', element: <ForgotPasswordPage /> },
|
||||
{ path: '/login', element: <LoginPage appName="Backoffice" enableSignup={false} /> },
|
||||
{ path: '/forgot-password', element: <ForgotPasswordPage appName="Backoffice" /> },
|
||||
{ path: '/otp-verify', element: <OTPVerificationPage /> },
|
||||
],
|
||||
},
|
||||
|
||||
@@ -4,8 +4,8 @@ import type { ReactNode } from 'react';
|
||||
import { authStorage } from '@ema-platform/auth';
|
||||
|
||||
const ROUTE_ACCESS: Record<string, string[]> = {
|
||||
SEAFARER: ['/dashboard', '/seafarer-', '/seaman-book', '/documents', '/certificates', '/endorsements', '/notifications', '/basic-safety-training'],
|
||||
VESSEL_OWNER: ['/vessel-owner', '/vessel-registration'],
|
||||
SEAFARER: ['/','/dashboard', '/seafarer-', '/seaman-book', '/documents', '/certificates', '/endorsements', '/notifications', '/basic-safety-training'],
|
||||
VESSEL_OWNER: ['/','/dashboard', '/vessel-owner', '/vessel-registration'],
|
||||
};
|
||||
|
||||
const SHARED = ['/profile', '/support'];
|
||||
|
||||
11
apps/portal/src/app/components/SmartDashboard.tsx
Normal file
11
apps/portal/src/app/components/SmartDashboard.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import { Navigate } from 'react-router-dom';
|
||||
import { authStorage } from '@ema-platform/auth';
|
||||
import { DashboardPage } from '../features/dashboard/pages/DashboardPage';
|
||||
|
||||
export function SmartDashboard() {
|
||||
const profile = authStorage.getProfile<{ type: string }>();
|
||||
if (profile?.type === 'VESSEL_OWNER') {
|
||||
return <Navigate to="/vessel-owner/dashboard" replace />;
|
||||
}
|
||||
return <DashboardPage />;
|
||||
}
|
||||
@@ -116,7 +116,7 @@ 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 }; type?: string }>>([]);
|
||||
const [professionsLoading, setProfessionsLoading] = useState(true);
|
||||
const [profileTrigger] = useApiMutation<{ id: string }>();
|
||||
const [addressTrigger] = useApiMutation<unknown>();
|
||||
@@ -147,6 +147,14 @@ export function ProfileSetupPage() {
|
||||
return map;
|
||||
}, [professions]);
|
||||
|
||||
const professionTypeMap = useMemo(() => {
|
||||
const map: Record<string, string> = {};
|
||||
professions.forEach((p) => {
|
||||
if (p.type) map[p.id] = p.type;
|
||||
});
|
||||
return map;
|
||||
}, [professions]);
|
||||
|
||||
const nameParts = useMemo(() => (user?.name?.en || '').trim().split(/\s+/), [user]);
|
||||
const profileDefaults: ProfileValues = useMemo(() => ({
|
||||
professionId: '',
|
||||
@@ -218,13 +226,14 @@ export function ProfileSetupPage() {
|
||||
const pv = profileWatch();
|
||||
const av = addressWatch();
|
||||
const selectedProfessionName = professionNameMap[pv.professionId] ?? '';
|
||||
const selectedType = professionTypeMap[pv.professionId] || 'SEAFARER';
|
||||
|
||||
const profileResult = await profileTrigger({
|
||||
url: '/profiles',
|
||||
method: 'POST',
|
||||
body: {
|
||||
userId: user?.id,
|
||||
type: 'SEAFARER',
|
||||
type: selectedType,
|
||||
professionId: pv.professionId,
|
||||
firstName: pv.firstName,
|
||||
middleName: pv.middleName,
|
||||
@@ -261,8 +270,7 @@ export function ProfileSetupPage() {
|
||||
}).unwrap();
|
||||
|
||||
notify.success('Profile setup complete!');
|
||||
//TODO: update the comparision
|
||||
navigate('SEAFARER' === 'VESSEL_OWNER' ? '/vessel-owner/dashboard' : '/dashboard');
|
||||
navigate('/dashboard');
|
||||
} catch {
|
||||
notify.error('Failed to save profile. Please try again.');
|
||||
} finally {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Provider } from 'react-redux';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { AuthConfigProvider } from '@ema-platform/auth';
|
||||
import type { ReactNode } from 'react';
|
||||
import { store } from '../store';
|
||||
import { MantineThemeProvider } from './MantineThemeProvider';
|
||||
@@ -15,17 +14,7 @@ export function AppProviders({ children }: { children: ReactNode }) {
|
||||
<ErrorBoundary>
|
||||
<Provider store={store}>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<AuthConfigProvider
|
||||
value={{
|
||||
appName: 'Portal',
|
||||
storagePrefix: 'ema-portal',
|
||||
loginRedirectPath: '/dashboard',
|
||||
enableSignup: true,
|
||||
enableForgotPassword: true,
|
||||
}}
|
||||
>
|
||||
<MantineThemeProvider>{children}</MantineThemeProvider>
|
||||
</AuthConfigProvider>
|
||||
<MantineThemeProvider>{children}</MantineThemeProvider>
|
||||
</QueryClientProvider>
|
||||
</Provider>
|
||||
</ErrorBoundary>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { i18n } from './i18n/config';
|
||||
import { PortalLayout } from './layouts/PortalLayout';
|
||||
import { ProtectedRoute } from './components/ProtectedRoute';
|
||||
import { ProfileGuard } from './components/ProfileGuard';
|
||||
import { SmartDashboard } from './components/SmartDashboard';
|
||||
|
||||
// Auth (standalone pages, no portal chrome)
|
||||
import { LoginPage, SignupPage, OTPVerificationPage, ForgotPasswordPage } from '@ema-platform/auth';
|
||||
@@ -12,7 +13,6 @@ import { LoginPage, SignupPage, OTPVerificationPage, ForgotPasswordPage } from '
|
||||
import { ProfileSetupPage } from './features/profile-setup/pages/ProfileSetupPage';
|
||||
|
||||
// Portal feature pages
|
||||
import { DashboardPage } from './features/dashboard/pages/DashboardPage';
|
||||
import { ProfilePage } from './features/profile/pages/ProfilePage';
|
||||
import { SupportPage } from './features/support/pages/SupportPage';
|
||||
import { SeafarerRegistrationPage } from './features/seafarer/pages/SeafarerRegistrationPage';
|
||||
@@ -45,11 +45,11 @@ import { VesselOwnerDashboardPage } from './features/vessel-owner/pages/VesselOw
|
||||
|
||||
export const router = createBrowserRouter([
|
||||
// Public auth pages
|
||||
{ path: '/login', element: <LoginPage /> },
|
||||
{ path: '/signup', element: <SignupPage /> },
|
||||
{ path: '/login', element: <LoginPage appName="Portal" /> },
|
||||
{ path: '/signup', element: <SignupPage appName="Portal" /> },
|
||||
|
||||
// Public auth pages
|
||||
{ path: '/forgot-password', element: <ForgotPasswordPage /> },
|
||||
{ path: '/forgot-password', element: <ForgotPasswordPage appName="Portal" /> },
|
||||
|
||||
// Protected auth pages
|
||||
{
|
||||
@@ -73,8 +73,8 @@ export const router = createBrowserRouter([
|
||||
</ProtectedRoute>
|
||||
),
|
||||
children: [
|
||||
{ path: '/', element: <Navigate to="/dashboard" replace /> },
|
||||
{ path: '/dashboard', element: <DashboardPage /> },
|
||||
{ path: '/', element: <SmartDashboard /> },
|
||||
{ path: '/dashboard', element: <SmartDashboard /> },
|
||||
|
||||
// Seafarer
|
||||
{ path: '/seafarer-registration', element: <SeafarerRegistrationPage /> },
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
export { AuthConfigProvider, useAuthConfig } from './lib/AuthConfig';
|
||||
export type { AuthConfigValue } from './lib/AuthConfig';
|
||||
export { AuthShell, BrandMark } from './lib/components/AuthShell';
|
||||
export { ProtectedRoute } from './lib/components/ProtectedRoute';
|
||||
export { LoginPage } from './lib/pages/LoginPage';
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
import { createContext, useContext, type ReactNode } from 'react';
|
||||
|
||||
export interface AuthConfigValue {
|
||||
appName: string;
|
||||
storagePrefix: string;
|
||||
loginRedirectPath: string;
|
||||
enableSignup: boolean;
|
||||
enableForgotPassword: boolean;
|
||||
logoUrl: string;
|
||||
}
|
||||
|
||||
const defaultConfig: AuthConfigValue = {
|
||||
appName: 'Portal',
|
||||
storagePrefix: 'ema-auth',
|
||||
loginRedirectPath: '/dashboard',
|
||||
enableSignup: true,
|
||||
enableForgotPassword: true,
|
||||
logoUrl: '/brand/ema-white.png',
|
||||
};
|
||||
|
||||
const AuthConfigContext = createContext<AuthConfigValue>(defaultConfig);
|
||||
|
||||
export function AuthConfigProvider({
|
||||
children,
|
||||
value,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
value: Partial<AuthConfigValue>;
|
||||
}) {
|
||||
const merged = { ...defaultConfig, ...value };
|
||||
return (
|
||||
<AuthConfigContext.Provider value={merged}>
|
||||
{children}
|
||||
</AuthConfigContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useAuthConfig() {
|
||||
return useContext(AuthConfigContext);
|
||||
}
|
||||
@@ -15,14 +15,14 @@ import {
|
||||
type BoxProps,
|
||||
} from '@mantine/core';
|
||||
import { IconCheck, IconMoon, IconSun } from '@tabler/icons-react';
|
||||
import { useAuthConfig } from '../AuthConfig';
|
||||
|
||||
const LOGO_URL = '/brand/ema-white.png';
|
||||
|
||||
export function BrandMark({ size = 44, ...boxProps }: BoxProps & { size?: number }) {
|
||||
const { logoUrl } = useAuthConfig();
|
||||
return (
|
||||
<Box
|
||||
component="img"
|
||||
src={logoUrl}
|
||||
src={LOGO_URL}
|
||||
alt="EMA"
|
||||
w={size}
|
||||
h={size}
|
||||
@@ -78,7 +78,6 @@ export function AuthShell({
|
||||
brandSubtitle = 'Apply for vessel and seafarer licenses, upload documents, and track every application in one secure portal.',
|
||||
}: AuthShellProps) {
|
||||
const theme = useMantineTheme();
|
||||
const { logoUrl } = useAuthConfig();
|
||||
const heroGradient = theme.other.heroGradient as string;
|
||||
const computed = useComputedColorScheme('light');
|
||||
const isDark = computed === 'dark';
|
||||
@@ -167,7 +166,7 @@ export function AuthShell({
|
||||
<Center>
|
||||
<Box
|
||||
component="img"
|
||||
src={logoUrl}
|
||||
src={LOGO_URL}
|
||||
alt="EMA Portal"
|
||||
style={{ display: 'block', maxWidth: '60%', height: 'auto' }}
|
||||
/>
|
||||
|
||||
@@ -24,7 +24,6 @@ import { Link } from 'react-router-dom';
|
||||
import { useApiMutation } from '@ema-platform/api';
|
||||
import { notify } from '@ema-platform/ui';
|
||||
import { AuthShell } from '../components/AuthShell';
|
||||
import { useAuthConfig } from '../AuthConfig';
|
||||
|
||||
const schema = z.object({
|
||||
email: z.string().email({ message: 'Enter a valid email' }),
|
||||
@@ -32,8 +31,11 @@ const schema = z.object({
|
||||
|
||||
type FormValues = z.infer<typeof schema>;
|
||||
|
||||
export function ForgotPasswordPage() {
|
||||
const { appName } = useAuthConfig();
|
||||
interface ForgotPasswordPageProps {
|
||||
appName?: string;
|
||||
}
|
||||
|
||||
export function ForgotPasswordPage({ appName = 'Portal' }: ForgotPasswordPageProps) {
|
||||
const [forgotTrigger, { isLoading }] = useApiMutation();
|
||||
const [sentTo, setSentTo] = useState<string | null>(null);
|
||||
const [serverError, setServerError] = useState<string | null>(null);
|
||||
|
||||
@@ -28,7 +28,6 @@ import { notify } from '@ema-platform/ui';
|
||||
import { AuthShell } from '../components/AuthShell';
|
||||
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';
|
||||
|
||||
const schema = z.object({
|
||||
@@ -38,11 +37,19 @@ const schema = z.object({
|
||||
|
||||
type FormValues = z.infer<typeof schema>;
|
||||
|
||||
export function LoginPage() {
|
||||
interface LoginPageProps {
|
||||
appName?: string;
|
||||
enableSignup?: boolean;
|
||||
enableForgotPassword?: boolean;
|
||||
}
|
||||
|
||||
export function LoginPage({
|
||||
appName = 'Portal',
|
||||
enableSignup = true,
|
||||
enableForgotPassword = true,
|
||||
}: LoginPageProps) {
|
||||
const navigate = useNavigate();
|
||||
const dispatch = useDispatch();
|
||||
const { appName, loginRedirectPath, enableSignup, enableForgotPassword } =
|
||||
useAuthConfig();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [rememberMe, setRememberMe] = useState(true);
|
||||
const [serverError, setServerError] = useState<string | null>(null);
|
||||
@@ -75,7 +82,6 @@ export function LoginPage() {
|
||||
dispatch(setUser(me));
|
||||
|
||||
let hasProfile = false;
|
||||
let profileType: string | undefined;
|
||||
try {
|
||||
const q = `w=user_id:=:${me.id}&i=user,address,profession`;
|
||||
const result = await profileCheckTrigger({
|
||||
@@ -84,7 +90,6 @@ export function LoginPage() {
|
||||
}).unwrap();
|
||||
if (result.total > 0 && result.items.length > 0) {
|
||||
const profile = result.items[0];
|
||||
profileType = 'VESSEL_OWNER';
|
||||
authStorage.setProfileId(profile.id);
|
||||
dispatch(setCurrentProfile(profile));
|
||||
hasProfile = true;
|
||||
@@ -109,7 +114,7 @@ export function LoginPage() {
|
||||
return;
|
||||
}
|
||||
|
||||
navigate(profileType === 'VESSEL_OWNER' ? '/vessel-owner/dashboard' : loginRedirectPath);
|
||||
navigate('/dashboard');
|
||||
} catch (err: unknown) {
|
||||
const msg =
|
||||
(err as { data?: { message?: string } })?.data?.message ??
|
||||
|
||||
@@ -20,7 +20,6 @@ import { useNavigate, useLocation } from 'react-router-dom';
|
||||
import { useApiMutation } from '@ema-platform/api';
|
||||
import { notify } from '@ema-platform/ui';
|
||||
import { AuthShell } from '../components/AuthShell';
|
||||
import { useAuthConfig } from '../AuthConfig';
|
||||
|
||||
const CODE_LENGTH = 6;
|
||||
const RESEND_SECONDS = 30;
|
||||
@@ -36,7 +35,6 @@ type FormValues = z.infer<typeof schema>;
|
||||
export function OTPVerificationPage() {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const { loginRedirectPath } = useAuthConfig();
|
||||
const state = location.state as
|
||||
| { email?: string; phoneNumber?: string; needsProfile?: boolean }
|
||||
| null;
|
||||
@@ -73,7 +71,7 @@ export function OTPVerificationPage() {
|
||||
}).unwrap();
|
||||
|
||||
notify.success('Phone number verified successfully');
|
||||
navigate(needsProfile ? '/profile-setup' : loginRedirectPath);
|
||||
navigate(needsProfile ? '/profile-setup' : '/dashboard');
|
||||
} catch (err: unknown) {
|
||||
const msg =
|
||||
(err as { data?: { message?: string } })?.data?.message ??
|
||||
@@ -182,7 +180,7 @@ export function OTPVerificationPage() {
|
||||
variant="light"
|
||||
fullWidth
|
||||
size="md"
|
||||
onClick={() => navigate(loginRedirectPath)}
|
||||
onClick={() => navigate('/dashboard')}
|
||||
>
|
||||
Skip verification for now
|
||||
</Button>
|
||||
|
||||
@@ -30,7 +30,6 @@ import { notify } from '@ema-platform/ui';
|
||||
import { AuthShell } from '../components/AuthShell';
|
||||
import { loginSuccess, setUser } from '../store/auth.slice';
|
||||
import type { AuthUser } from '../types/auth.types';
|
||||
import { useAuthConfig } from '../AuthConfig';
|
||||
|
||||
const schema = z
|
||||
.object({
|
||||
@@ -63,10 +62,13 @@ interface SignupPayload {
|
||||
confirmPassword: string;
|
||||
}
|
||||
|
||||
export function SignupPage() {
|
||||
interface SignupPageProps {
|
||||
appName?: string;
|
||||
}
|
||||
|
||||
export function SignupPage({ appName = 'Portal' }: SignupPageProps) {
|
||||
const navigate = useNavigate();
|
||||
const dispatch = useDispatch();
|
||||
const { appName, loginRedirectPath } = useAuthConfig();
|
||||
const [agreed, setAgreed] = useState(false);
|
||||
const [serverError, setServerError] = useState<string | null>(null);
|
||||
const [signupTrigger, { isLoading: loading }] = useApiMutation<{
|
||||
|
||||
Reference in New Issue
Block a user