feat: implement tenant landing page to display and launch assigned modules

This commit is contained in:
Yashwin 2026-04-08 15:13:15 +05:30
parent 9067448e63
commit d26456ab94
4 changed files with 263 additions and 6 deletions

View File

@ -62,8 +62,8 @@ const Login = (): ReactElement => {
if (rolesArray.includes('super_admin')) { if (rolesArray.includes('super_admin')) {
navigate('/dashboard'); navigate('/dashboard');
} else { } else {
// Tenant admin - redirect to tenant dashboard // Tenant admin - redirect to tenant landing page (workspace selector)
navigate('/tenant'); navigate('/tenant/landing');
} }
} }
}, [isAuthenticated, roles, navigate]); }, [isAuthenticated, roles, navigate]);
@ -95,7 +95,7 @@ const Login = (): ReactElement => {
if (userRoles.includes('super_admin')) { if (userRoles.includes('super_admin')) {
navigate('/dashboard'); navigate('/dashboard');
} else { } else {
navigate('/tenant'); navigate('/tenant/landing');
} }
} }
} catch (error: any) { } catch (error: any) {

View 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;

View File

@ -68,8 +68,8 @@ const TenantLogin = (): ReactElement => {
if (rolesArray.includes('super_admin')) { if (rolesArray.includes('super_admin')) {
navigate('/dashboard'); navigate('/dashboard');
} else { } else {
// Tenant admin - redirect to tenant dashboard // Tenant admin - redirect to tenant landing page (workspace selector)
navigate('/tenant'); navigate('/tenant/landing');
} }
} }
}, [isAuthenticated, roles, navigate]); }, [isAuthenticated, roles, navigate]);
@ -109,7 +109,7 @@ const TenantLogin = (): ReactElement => {
if (rolesArray.includes('super_admin')) { if (rolesArray.includes('super_admin')) {
navigate('/dashboard'); navigate('/dashboard');
} else { } else {
navigate('/tenant'); navigate('/tenant/landing');
} }
} }
} catch (error: any) { } catch (error: any) {

View File

@ -8,6 +8,7 @@ const Settings = lazy(() => import("@/pages/tenant/Settings"));
const Users = lazy(() => import("@/pages/tenant/Users")); const Users = lazy(() => import("@/pages/tenant/Users"));
const AuditLogs = lazy(() => import("@/pages/tenant/AuditLogs")); const AuditLogs = lazy(() => import("@/pages/tenant/AuditLogs"));
const Modules = lazy(() => import("@/pages/tenant/Modules")); const Modules = lazy(() => import("@/pages/tenant/Modules"));
const LandingPage = lazy(() => import("@/pages/tenant/LandingPage"));
const Departments = lazy(() => import("@/pages/tenant/Departments")); const Departments = lazy(() => import("@/pages/tenant/Departments"));
const Designations = lazy(() => import("@/pages/tenant/Designations")); const Designations = lazy(() => import("@/pages/tenant/Designations"));
const WorkflowDefination = lazy( const WorkflowDefination = lazy(
@ -51,6 +52,10 @@ export interface RouteConfig {
// Tenant Admin routes (requires authentication but NOT super_admin role) // Tenant Admin routes (requires authentication but NOT super_admin role)
export const tenantAdminRoutes: RouteConfig[] = [ export const tenantAdminRoutes: RouteConfig[] = [
{
path: "/tenant/landing",
element: <LazyRoute component={LandingPage} />,
},
{ {
path: "/tenant", path: "/tenant",
element: <LazyRoute component={Dashboard} />, element: <LazyRoute component={Dashboard} />,