371 lines
15 KiB
TypeScript
371 lines
15 KiB
TypeScript
import React, { useMemo } from 'react';
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from './ui/card';
|
|
import { Badge } from './ui/badge';
|
|
import { Button } from './ui/button';
|
|
import { Progress } from './ui/progress';
|
|
import { Avatar, AvatarFallback, AvatarImage } from './ui/avatar';
|
|
import {
|
|
FileText,
|
|
Clock,
|
|
AlertTriangle,
|
|
TrendingUp,
|
|
CheckCircle,
|
|
Users,
|
|
Zap,
|
|
Shield,
|
|
ArrowRight,
|
|
Bell,
|
|
Star,
|
|
Activity,
|
|
Calendar,
|
|
Target,
|
|
Flame,
|
|
Settings
|
|
} from 'lucide-react';
|
|
|
|
interface DashboardProps {
|
|
onNavigate?: (page: string) => void;
|
|
onNewRequest?: () => void;
|
|
}
|
|
|
|
// Static data to prevent re-creation on each render
|
|
const STATS_DATA = [
|
|
{
|
|
title: 'Open Requests',
|
|
value: '24',
|
|
description: '+3 from last week',
|
|
icon: FileText,
|
|
trend: 'up',
|
|
color: 'text-emerald-600',
|
|
bgColor: 'bg-emerald-50',
|
|
change: '+12.5%'
|
|
},
|
|
{
|
|
title: 'Avg. SLA Compliance',
|
|
value: '87%',
|
|
description: '+5% from last month',
|
|
icon: Target,
|
|
trend: 'up',
|
|
color: 'text-blue-600',
|
|
bgColor: 'bg-blue-50',
|
|
change: '+5.8%'
|
|
},
|
|
{
|
|
title: 'High Priority Alerts',
|
|
value: '5',
|
|
description: 'Requires immediate attention',
|
|
icon: Flame,
|
|
trend: 'neutral',
|
|
color: 'text-red-600',
|
|
bgColor: 'bg-red-50',
|
|
change: '-2'
|
|
},
|
|
{
|
|
title: 'Approved This Month',
|
|
value: '142',
|
|
description: '+12% from last month',
|
|
icon: CheckCircle,
|
|
trend: 'up',
|
|
color: 'text-green-600',
|
|
bgColor: 'bg-green-50',
|
|
change: '+18.2%'
|
|
}
|
|
];
|
|
|
|
const RECENT_ACTIVITY = [
|
|
{
|
|
id: 'RE-REQ-024',
|
|
title: 'Marketing Campaign Approval',
|
|
user: 'Sarah Chen',
|
|
action: 'approved',
|
|
time: '2 minutes ago',
|
|
avatar: 'SC',
|
|
priority: 'high'
|
|
},
|
|
{
|
|
id: 'RE-REQ-023',
|
|
title: 'Budget Allocation Request',
|
|
user: 'Mike Johnson',
|
|
action: 'commented',
|
|
time: '15 minutes ago',
|
|
avatar: 'MJ',
|
|
priority: 'medium'
|
|
},
|
|
{
|
|
id: 'RE-REQ-022',
|
|
title: 'Vendor Contract Review',
|
|
user: 'David Kumar',
|
|
action: 'submitted',
|
|
time: '1 hour ago',
|
|
avatar: 'DK',
|
|
priority: 'low'
|
|
},
|
|
{
|
|
id: 'RE-REQ-021',
|
|
title: 'IT Equipment Purchase',
|
|
user: 'Lisa Wong',
|
|
action: 'escalated',
|
|
time: '2 hours ago',
|
|
avatar: 'LW',
|
|
priority: 'high'
|
|
},
|
|
{
|
|
id: 'RE-REQ-020',
|
|
title: 'Office Space Lease',
|
|
user: 'John Doe',
|
|
action: 'rejected',
|
|
time: '3 hours ago',
|
|
avatar: 'JD',
|
|
priority: 'medium'
|
|
}
|
|
];
|
|
|
|
const HIGH_PRIORITY_REQUESTS = [
|
|
{ id: 'RE-REQ-001', title: 'Emergency Equipment Purchase', sla: '2 hours left', progress: 85 },
|
|
{ id: 'RE-REQ-005', title: 'Critical Vendor Agreement', sla: '4 hours left', progress: 60 },
|
|
{ id: 'RE-REQ-012', title: 'Urgent Marketing Approval', sla: '6 hours left', progress: 40 }
|
|
];
|
|
|
|
// Utility functions outside component
|
|
const getActionColor = (action: string) => {
|
|
switch (action) {
|
|
case 'approved': return 'text-emerald-700 bg-emerald-100 border-emerald-200';
|
|
case 'rejected': return 'text-red-700 bg-red-100 border-red-200';
|
|
case 'commented': return 'text-blue-700 bg-blue-100 border-blue-200';
|
|
case 'escalated': return 'text-orange-700 bg-orange-100 border-orange-200';
|
|
case 'submitted': return 'text-purple-700 bg-purple-100 border-purple-200';
|
|
default: return 'text-gray-700 bg-gray-100 border-gray-200';
|
|
}
|
|
};
|
|
|
|
const getPriorityColor = (priority: string) => {
|
|
switch (priority) {
|
|
case 'high': return 'bg-red-100 text-red-800 border-red-200';
|
|
case 'medium': return 'bg-orange-100 text-orange-800 border-orange-200';
|
|
case 'low': return 'bg-green-100 text-green-800 border-green-200';
|
|
default: return 'bg-gray-100 text-gray-800 border-gray-200';
|
|
}
|
|
};
|
|
|
|
export function Dashboard({ onNavigate, onNewRequest }: DashboardProps) {
|
|
// Memoize quick actions to prevent recreation on each render
|
|
const quickActions = useMemo(() => [
|
|
{ label: 'New Request', icon: FileText, action: () => onNewRequest?.(), color: 'bg-emerald-600 hover:bg-emerald-700' },
|
|
{ label: 'View Pending', icon: Clock, action: () => onNavigate?.('open-requests'), color: 'bg-blue-600 hover:bg-blue-700' },
|
|
{ label: 'Reports', icon: Activity, action: () => {}, color: 'bg-purple-600 hover:bg-purple-700' },
|
|
{ label: 'Settings', icon: Settings, action: () => {}, color: 'bg-slate-600 hover:bg-slate-700' }
|
|
], [onNavigate, onNewRequest]);
|
|
|
|
return (
|
|
<div className="space-y-6 max-w-7xl mx-auto p-4">
|
|
{/* Hero Section with Clear Background */}
|
|
<Card className="relative overflow-hidden shadow-xl border-0">
|
|
<div className="absolute inset-0 bg-gradient-to-br from-slate-900 via-slate-800 to-slate-900"></div>
|
|
|
|
<CardContent className="relative z-10 p-8 lg:p-12">
|
|
<div className="flex flex-col lg:flex-row items-start lg:items-center justify-between gap-6">
|
|
<div className="text-white">
|
|
<div className="flex items-center gap-4 mb-6">
|
|
<div className="w-16 h-16 bg-yellow-400 rounded-xl flex items-center justify-center shadow-lg">
|
|
<Shield className="w-8 h-8 text-slate-900" />
|
|
</div>
|
|
<div>
|
|
<h1 className="text-4xl font-bold mb-2 text-white">Welcome to Royal Enfield Portal</h1>
|
|
<p className="text-xl text-gray-200">Rev up your workflow with streamlined approvals</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex flex-wrap gap-4 mt-8">
|
|
{quickActions.map((action, index) => (
|
|
<Button
|
|
key={index}
|
|
onClick={action.action}
|
|
className={`${action.color} text-white border-0 shadow-lg hover:shadow-xl transition-all duration-200`}
|
|
size="lg"
|
|
>
|
|
<action.icon className="w-5 h-5 mr-2" />
|
|
{action.label}
|
|
</Button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Decorative Elements */}
|
|
<div className="hidden lg:flex items-center gap-4">
|
|
<div className="w-24 h-24 bg-yellow-400/20 rounded-full flex items-center justify-center">
|
|
<div className="w-16 h-16 bg-yellow-400/30 rounded-full flex items-center justify-center">
|
|
<Zap className="w-8 h-8 text-yellow-400" />
|
|
</div>
|
|
</div>
|
|
<div className="w-16 h-16 bg-white/10 rounded-full flex items-center justify-center">
|
|
<Activity className="w-6 h-6 text-white/80" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Stats Cards with Better Contrast */}
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
|
|
{STATS_DATA.map((stat, index) => (
|
|
<Card key={index} className="hover:shadow-xl transition-all duration-300 hover:scale-105 cursor-pointer shadow-lg">
|
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
<CardTitle className="text-sm font-medium text-muted-foreground">
|
|
{stat.title}
|
|
</CardTitle>
|
|
<div className={`p-3 rounded-lg ${stat.bgColor} shadow-sm`}>
|
|
<stat.icon className={`h-5 w-5 ${stat.color}`} />
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="flex items-end justify-between">
|
|
<div>
|
|
<div className="text-3xl font-bold text-gray-900 mb-1">{stat.value}</div>
|
|
<div className="flex items-center gap-2">
|
|
{stat.trend === 'up' && (
|
|
<div className="flex items-center gap-1 text-emerald-600">
|
|
<TrendingUp className="h-3 w-3" />
|
|
<span className="text-xs font-medium">{stat.change}</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
<ArrowRight className="h-4 w-4 text-muted-foreground hover:text-emerald-600 transition-colors" />
|
|
</div>
|
|
<p className="text-xs text-muted-foreground mt-2">{stat.description}</p>
|
|
</CardContent>
|
|
</Card>
|
|
))}
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
|
{/* High Priority Alerts */}
|
|
<Card className="lg:col-span-1 shadow-lg hover:shadow-xl transition-shadow">
|
|
<CardHeader className="pb-4">
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-3">
|
|
<div className="p-3 bg-red-100 rounded-lg">
|
|
<Flame className="h-5 w-5 text-red-600" />
|
|
</div>
|
|
<div>
|
|
<CardTitle className="text-lg text-gray-900">Critical Alerts</CardTitle>
|
|
<CardDescription className="text-gray-600">Urgent attention required</CardDescription>
|
|
</div>
|
|
</div>
|
|
<Badge variant="destructive" className="animate-pulse font-semibold">
|
|
{HIGH_PRIORITY_REQUESTS.length}
|
|
</Badge>
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent className="space-y-4">
|
|
{HIGH_PRIORITY_REQUESTS.map((request) => (
|
|
<div key={request.id} className="p-4 bg-red-50 rounded-xl border border-red-100 hover:shadow-md transition-all duration-200">
|
|
<div className="flex items-start justify-between gap-2 mb-3">
|
|
<div className="flex-1 min-w-0">
|
|
<div className="flex items-center gap-2 mb-1">
|
|
<p className="font-semibold text-sm text-gray-900">{request.id}</p>
|
|
<Star className="h-3 w-3 text-red-500" />
|
|
</div>
|
|
<p className="text-sm text-gray-700">{request.title}</p>
|
|
</div>
|
|
<Badge variant="outline" className="text-xs bg-white border-red-200 text-red-700 font-medium">
|
|
{request.sla}
|
|
</Badge>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<div className="flex justify-between text-xs text-gray-600">
|
|
<span>Progress</span>
|
|
<span className="font-medium">{request.progress}%</span>
|
|
</div>
|
|
<Progress
|
|
value={request.progress}
|
|
className="h-2"
|
|
/>
|
|
</div>
|
|
</div>
|
|
))}
|
|
<Button
|
|
variant="outline"
|
|
className="w-full mt-4 border-red-200 text-red-700 hover:bg-red-50 hover:border-red-300 font-medium"
|
|
onClick={() => onNavigate?.('open-requests')}
|
|
>
|
|
<AlertTriangle className="w-4 h-4 mr-2" />
|
|
View All Critical Items
|
|
</Button>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Recent Activity */}
|
|
<Card className="lg:col-span-2 shadow-lg hover:shadow-xl transition-shadow">
|
|
<CardHeader className="pb-4">
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-3">
|
|
<div className="p-3 bg-blue-100 rounded-lg">
|
|
<Activity className="h-5 w-5 text-blue-600" />
|
|
</div>
|
|
<div>
|
|
<CardTitle className="text-lg text-gray-900">Recent Activity</CardTitle>
|
|
<CardDescription className="text-gray-600">Latest updates across all requests</CardDescription>
|
|
</div>
|
|
</div>
|
|
<Button variant="ghost" size="sm" className="text-blue-600 hover:bg-blue-50 font-medium">
|
|
<Calendar className="w-4 h-4 mr-2" />
|
|
View All
|
|
</Button>
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="space-y-3">
|
|
{RECENT_ACTIVITY.map((activity) => (
|
|
<div
|
|
key={activity.id}
|
|
className="flex items-center gap-4 p-4 rounded-xl hover:bg-gray-50 transition-all duration-200 cursor-pointer border border-transparent hover:border-gray-200"
|
|
>
|
|
<div className="relative">
|
|
<Avatar className="h-10 w-10 ring-2 ring-white shadow-md">
|
|
<AvatarImage src="" />
|
|
<AvatarFallback className="bg-slate-700 text-white font-semibold">
|
|
{activity.avatar}
|
|
</AvatarFallback>
|
|
</Avatar>
|
|
<div className="absolute -bottom-1 -right-1 w-4 h-4 bg-white rounded-full flex items-center justify-center shadow-sm">
|
|
{activity.action === 'approved' && <CheckCircle className="w-3 h-3 text-green-600" />}
|
|
{activity.action === 'rejected' && <AlertTriangle className="w-3 h-3 text-red-600" />}
|
|
{activity.action === 'commented' && <Bell className="w-3 h-3 text-blue-600" />}
|
|
{activity.action === 'escalated' && <Flame className="w-3 h-3 text-orange-600" />}
|
|
{activity.action === 'submitted' && <FileText className="w-3 h-3 text-purple-600" />}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex-1 min-w-0">
|
|
<div className="flex items-center gap-2 mb-1 flex-wrap">
|
|
<span className="font-semibold text-sm text-gray-900">{activity.id}</span>
|
|
<Badge
|
|
variant="outline"
|
|
className={`text-xs border ${getActionColor(activity.action)} font-medium`}
|
|
>
|
|
{activity.action}
|
|
</Badge>
|
|
<Badge
|
|
variant="outline"
|
|
className={`text-xs ${getPriorityColor(activity.priority)} font-medium`}
|
|
>
|
|
{activity.priority}
|
|
</Badge>
|
|
</div>
|
|
<p className="text-sm text-gray-700 line-clamp-1 mb-1">{activity.title}</p>
|
|
<p className="text-xs text-muted-foreground">
|
|
by <span className="font-medium text-gray-900">{activity.user}</span> • {activity.time}
|
|
</p>
|
|
</div>
|
|
|
|
<ArrowRight className="h-4 w-4 text-muted-foreground hover:text-blue-600 transition-colors" />
|
|
</div>
|
|
))}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
</div>
|
|
);
|
|
} |