feat: implement tenant landing page to display and launch assigned modules
This commit is contained in:
parent
9067448e63
commit
d26456ab94
@ -62,8 +62,8 @@ const Login = (): ReactElement => {
|
||||
if (rolesArray.includes('super_admin')) {
|
||||
navigate('/dashboard');
|
||||
} else {
|
||||
// Tenant admin - redirect to tenant dashboard
|
||||
navigate('/tenant');
|
||||
// Tenant admin - redirect to tenant landing page (workspace selector)
|
||||
navigate('/tenant/landing');
|
||||
}
|
||||
}
|
||||
}, [isAuthenticated, roles, navigate]);
|
||||
@ -95,7 +95,7 @@ const Login = (): ReactElement => {
|
||||
if (userRoles.includes('super_admin')) {
|
||||
navigate('/dashboard');
|
||||
} else {
|
||||
navigate('/tenant');
|
||||
navigate('/tenant/landing');
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
|
||||
252
src/pages/tenant/LandingPage.tsx
Normal file
252
src/pages/tenant/LandingPage.tsx
Normal file
@ -0,0 +1,252 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import type { ReactElement } from 'react';
|
||||
import {
|
||||
ArrowRight,
|
||||
Layout as LayoutIcon,
|
||||
Shield,
|
||||
FileText,
|
||||
LayoutGrid,
|
||||
Database,
|
||||
ClipboardCheck,
|
||||
Package,
|
||||
ArrowUpRight
|
||||
} from 'lucide-react';
|
||||
import { useAppSelector } from '@/hooks/redux-hooks';
|
||||
import { moduleService } from '@/services/module-service';
|
||||
import type { MyModule } from '@/types/module';
|
||||
import { AuthenticatedImage } from '@/components/shared';
|
||||
|
||||
interface WorkspaceCardProps {
|
||||
title: string;
|
||||
description: string;
|
||||
icon: React.ReactNode;
|
||||
iconBg: string;
|
||||
iconColor: string;
|
||||
onClick: () => void;
|
||||
isExternal?: boolean;
|
||||
}
|
||||
|
||||
const WorkspaceCard = ({
|
||||
title,
|
||||
description,
|
||||
icon,
|
||||
iconBg,
|
||||
iconColor,
|
||||
onClick,
|
||||
isExternal = false
|
||||
}: WorkspaceCardProps) => (
|
||||
<div
|
||||
className="group bg-white border border-[rgba(0,0,0,0.08)] rounded-[24px] p-8 flex flex-col h-full transition-all hover:border-[rgba(0,0,0,0.15)] hover:shadow-[0px_8px_32px_0px_rgba(0,0,0,0.04)] cursor-pointer"
|
||||
onClick={onClick}
|
||||
>
|
||||
<div className="flex-1">
|
||||
<div
|
||||
className="w-12 h-12 rounded-xl flex items-center justify-center mb-6 transition-transform group-hover:scale-105"
|
||||
style={{ backgroundColor: iconBg }}
|
||||
>
|
||||
<div style={{ color: iconColor }}>{icon}</div>
|
||||
</div>
|
||||
<h3 className="text-xl font-semibold text-[#0f1724] mb-3">{title}</h3>
|
||||
<p className="text-[#64748b] text-sm leading-relaxed mb-8 line-clamp-3">
|
||||
{description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between pt-6 border-t border-[rgba(0,0,0,0.08)]">
|
||||
<span className="text-sm font-semibold text-[#0f1724]">Open Workspace</span>
|
||||
<div className="w-8 h-8 rounded-full flex items-center justify-center text-[#64748b] group-hover:text-[#0f1724] transition-colors">
|
||||
{isExternal ? <ArrowUpRight className="w-5 h-5" /> : <ArrowRight className="w-5 h-5 transition-transform group-hover:translate-x-1" />}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const LandingPage = (): ReactElement => {
|
||||
const navigate = useNavigate();
|
||||
const { user, roles, tenantId } = useAppSelector((state) => state.auth);
|
||||
const { logoUrl } = useAppSelector((state) => state.theme);
|
||||
const [modules, setModules] = useState<MyModule[]>([]);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchModules = async () => {
|
||||
try {
|
||||
const response = await moduleService.getMyModules();
|
||||
if (response.success && response.data) {
|
||||
setModules(response.data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch modules:', error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
fetchModules();
|
||||
}, []);
|
||||
|
||||
const handleLaunchModule = async (moduleId: string) => {
|
||||
try {
|
||||
let rolesArray: string[] = [];
|
||||
if (Array.isArray(roles)) {
|
||||
rolesArray = roles;
|
||||
} else if (typeof roles === 'string') {
|
||||
try { rolesArray = JSON.parse(roles); } catch { rolesArray = []; }
|
||||
}
|
||||
|
||||
const isSuperAdmin = rolesArray.includes('super_admin');
|
||||
const launchTenantId = isSuperAdmin ? tenantId : null;
|
||||
|
||||
const response = await moduleService.launch(moduleId, launchTenantId);
|
||||
if (response.success && response.data.launch_url) {
|
||||
window.open(response.data.launch_url, '_blank', 'noopener,noreferrer');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to launch module:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const getUserName = () => {
|
||||
if (user?.first_name) return user.first_name;
|
||||
return user?.email?.split('@')[0] || 'User';
|
||||
};
|
||||
|
||||
const getRoleDisplayName = () => {
|
||||
let rolesArray: string[] = [];
|
||||
if (Array.isArray(roles)) {
|
||||
rolesArray = roles;
|
||||
} else if (typeof roles === 'string') {
|
||||
try { rolesArray = JSON.parse(roles); } catch { rolesArray = []; }
|
||||
}
|
||||
const role = rolesArray[0] || 'User';
|
||||
return role.split('_').map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join(' ');
|
||||
};
|
||||
|
||||
// Icon mapping for modules based on code or name
|
||||
const getModuleIcon = (module: MyModule) => {
|
||||
const code = module.name.toUpperCase();
|
||||
if (code.includes('CER') || code.includes('ENTITY')) return <Database className="w-6 h-6" />;
|
||||
if (code.includes('COC') || code.includes('CHAIN')) return <Shield className="w-6 h-6" />;
|
||||
if (code.includes('LSR') || code.includes('SERVICE')) return <FileText className="w-6 h-6" />;
|
||||
if (code.includes('DCT') || code.includes('DATA')) return <LayoutGrid className="w-6 h-6" />;
|
||||
if (code.includes('TMT') || code.includes('TASK')) return <ClipboardCheck className="w-6 h-6" />;
|
||||
return <Package className="w-6 h-6" />;
|
||||
};
|
||||
|
||||
const getModuleColor = (index: number) => {
|
||||
const colors = [
|
||||
{ bg: '#ECFDF5', text: '#059669' }, // Green
|
||||
{ bg: '#FEF2F2', text: '#DC2626' }, // Red
|
||||
{ bg: '#F5F3FF', text: '#7C3AED' }, // Purple
|
||||
{ bg: '#F0FDFA', text: '#0D9488' }, // Teal
|
||||
{ bg: '#FEFCE8', text: '#CA8A04' }, // Yellow
|
||||
];
|
||||
return colors[index % colors.length];
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[#F8FAFC] flex flex-col">
|
||||
{/* Landing Header */}
|
||||
<header className="bg-white/80 backdrop-blur-sm border-b border-[rgba(0,0,0,0.08)] py-3 px-6 flex items-center justify-between sticky top-0 z-50">
|
||||
<div className="flex items-center gap-3">
|
||||
{logoUrl ? (
|
||||
<AuthenticatedImage
|
||||
src={logoUrl}
|
||||
alt="Logo"
|
||||
className="h-8 w-auto max-w-[150px] object-contain"
|
||||
fallback={
|
||||
<div className="w-8 h-8 rounded-lg flex items-center justify-center shadow-sm bg-[#112868]">
|
||||
<Shield className="w-5 h-5 text-white" strokeWidth={1.67} />
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<div className="w-8 h-8 rounded-lg flex items-center justify-center shadow-sm bg-[#112868]">
|
||||
<Shield className="w-5 h-5 text-white" strokeWidth={1.67} />
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-col">
|
||||
<span className="text-sm font-bold text-[#0f1724] leading-tight">QAssure</span>
|
||||
<span className="text-[10px] font-semibold text-[#6b7280] uppercase tracking-wider">
|
||||
{getRoleDisplayName()}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="text-right hidden sm:block">
|
||||
<p className="text-sm font-semibold text-[#0f1724]">{user?.first_name ? `${user.first_name} ${user.last_name || ''}` : getUserName()}</p>
|
||||
<p className="text-[11px] text-[#6b7280]">{user?.email}</p>
|
||||
</div>
|
||||
<div className="w-9 h-9 bg-[#f1f5f9] rounded-full flex items-center justify-center border border-[rgba(0,0,0,0.08)]">
|
||||
<span className="text-xs font-medium text-[#0f1724]">
|
||||
{user?.first_name ? user.first_name[0].toUpperCase() : 'U'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main className="flex-1 flex flex-col items-center px-6 py-16 md:py-24">
|
||||
{/* Welcome Section */}
|
||||
<div className="text-center mb-16 md:mb-24 animate-in fade-in slide-in-from-bottom-4 duration-700">
|
||||
<h1 className="text-4xl md:text-5xl font-bold text-[#0f1724] mb-4">
|
||||
Welcome back, {getUserName()}
|
||||
</h1>
|
||||
<p className="text-[#64748b] text-lg max-w-2xl mx-auto leading-relaxed">
|
||||
Select a module below to access your workspace. You can switch between modules anytime from the main navigation.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Workspace Grid */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8 w-full max-w-7xl animate-in fade-in slide-in-from-bottom-8 duration-1000 delay-200">
|
||||
{/* Platform Administration Card */}
|
||||
<WorkspaceCard
|
||||
title="Platform Administration"
|
||||
description="Manage organizations, user access, billing, and global workspace settings."
|
||||
icon={<LayoutIcon className="w-6 h-6" />}
|
||||
iconBg="#EFF6FF"
|
||||
iconColor="#2563EB"
|
||||
onClick={() => navigate('/tenant')}
|
||||
/>
|
||||
|
||||
{/* Dynamic Module Cards */}
|
||||
{!isLoading && modules.map((module, index) => {
|
||||
const color = getModuleColor(index);
|
||||
return (
|
||||
<WorkspaceCard
|
||||
key={module.id}
|
||||
title={module.name}
|
||||
description={module.description || `Access and manage your ${module.name} workspace details and operations.`}
|
||||
icon={getModuleIcon(module)}
|
||||
iconBg={color.bg}
|
||||
iconColor={color.text}
|
||||
isExternal={true}
|
||||
onClick={() => handleLaunchModule(module.id)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Loading States */}
|
||||
{isLoading && [1, 2].map((i) => (
|
||||
<div key={i} className="bg-white border border-[rgba(0,0,0,0.08)] rounded-[24px] p-8 h-[320px] animate-pulse">
|
||||
<div className="w-12 h-12 bg-gray-100 rounded-xl mb-6" />
|
||||
<div className="h-6 bg-gray-100 rounded w-3/4 mb-4" />
|
||||
<div className="h-4 bg-gray-100 rounded w-full mb-2" />
|
||||
<div className="h-4 bg-gray-100 rounded w-5/6" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</main>
|
||||
|
||||
{/* Footer Decoration */}
|
||||
<footer className="py-12 flex justify-center opacity-40 grayscale pointer-events-none">
|
||||
<div className="flex items-center gap-2">
|
||||
<Shield className="w-5 h-5" />
|
||||
<span className="text-sm font-semibold uppercase tracking-wider">QAssure Secure Workspace</span>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LandingPage;
|
||||
@ -68,8 +68,8 @@ const TenantLogin = (): ReactElement => {
|
||||
if (rolesArray.includes('super_admin')) {
|
||||
navigate('/dashboard');
|
||||
} else {
|
||||
// Tenant admin - redirect to tenant dashboard
|
||||
navigate('/tenant');
|
||||
// Tenant admin - redirect to tenant landing page (workspace selector)
|
||||
navigate('/tenant/landing');
|
||||
}
|
||||
}
|
||||
}, [isAuthenticated, roles, navigate]);
|
||||
@ -109,7 +109,7 @@ const TenantLogin = (): ReactElement => {
|
||||
if (rolesArray.includes('super_admin')) {
|
||||
navigate('/dashboard');
|
||||
} else {
|
||||
navigate('/tenant');
|
||||
navigate('/tenant/landing');
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
|
||||
@ -8,6 +8,7 @@ const Settings = lazy(() => import("@/pages/tenant/Settings"));
|
||||
const Users = lazy(() => import("@/pages/tenant/Users"));
|
||||
const AuditLogs = lazy(() => import("@/pages/tenant/AuditLogs"));
|
||||
const Modules = lazy(() => import("@/pages/tenant/Modules"));
|
||||
const LandingPage = lazy(() => import("@/pages/tenant/LandingPage"));
|
||||
const Departments = lazy(() => import("@/pages/tenant/Departments"));
|
||||
const Designations = lazy(() => import("@/pages/tenant/Designations"));
|
||||
const WorkflowDefination = lazy(
|
||||
@ -51,6 +52,10 @@ export interface RouteConfig {
|
||||
|
||||
// Tenant Admin routes (requires authentication but NOT super_admin role)
|
||||
export const tenantAdminRoutes: RouteConfig[] = [
|
||||
{
|
||||
path: "/tenant/landing",
|
||||
element: <LazyRoute component={LandingPage} />,
|
||||
},
|
||||
{
|
||||
path: "/tenant",
|
||||
element: <LazyRoute component={Dashboard} />,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user