534 lines
18 KiB
TypeScript
534 lines
18 KiB
TypeScript
|
|
import React, { use, useState } from 'react';
|
||
|
|
import { AuthenticatedLayout } from '../layout/AuthenticatedLayout';
|
||
|
|
import { Card, CardContent, CardHeader, CardTitle } from '../ui/card';
|
||
|
|
import { Button } from '../ui/button';
|
||
|
|
import { Badge } from '../ui/badge';
|
||
|
|
import { Alert, AlertDescription } from '../ui/alert';
|
||
|
|
import { Progress } from '../ui/progress';
|
||
|
|
import {
|
||
|
|
Users,
|
||
|
|
BookOpen,
|
||
|
|
DollarSign,
|
||
|
|
GraduationCap,
|
||
|
|
TrendingUp,
|
||
|
|
AlertTriangle,
|
||
|
|
CheckCircle,
|
||
|
|
Clock,
|
||
|
|
Eye,
|
||
|
|
X,
|
||
|
|
Plus,
|
||
|
|
FileText,
|
||
|
|
Upload,
|
||
|
|
Globe,
|
||
|
|
Building,
|
||
|
|
ExternalLink,
|
||
|
|
Download,
|
||
|
|
MoreHorizontal
|
||
|
|
} from 'lucide-react';
|
||
|
|
import {
|
||
|
|
DropdownMenu,
|
||
|
|
DropdownMenuContent,
|
||
|
|
DropdownMenuItem,
|
||
|
|
DropdownMenuTrigger,
|
||
|
|
} from '../ui/dropdown-menu';
|
||
|
|
import { useNavigate } from 'react-router-dom';
|
||
|
|
|
||
|
|
interface DashboardProps {
|
||
|
|
// onNavigate: (route: string) => void;
|
||
|
|
onLogout: () => void;
|
||
|
|
user: any;
|
||
|
|
}
|
||
|
|
|
||
|
|
interface KPITile {
|
||
|
|
title: string;
|
||
|
|
value: string;
|
||
|
|
change: string;
|
||
|
|
trend: 'up' | 'down' | 'neutral';
|
||
|
|
icon: React.ComponentType<{ className?: string }>;
|
||
|
|
route: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
interface AlertBanner {
|
||
|
|
id: string;
|
||
|
|
type: 'info' | 'warning' | 'error';
|
||
|
|
title: string;
|
||
|
|
message: string;
|
||
|
|
dismissible: boolean;
|
||
|
|
}
|
||
|
|
|
||
|
|
interface TaskQueue {
|
||
|
|
title: string;
|
||
|
|
count: number;
|
||
|
|
route: string;
|
||
|
|
urgent?: boolean;
|
||
|
|
}
|
||
|
|
|
||
|
|
interface ActivityItem {
|
||
|
|
id: string;
|
||
|
|
type: string;
|
||
|
|
user: string;
|
||
|
|
action: string;
|
||
|
|
target: string;
|
||
|
|
timestamp: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
interface QuickAction {
|
||
|
|
title: string;
|
||
|
|
description: string;
|
||
|
|
icon: React.ComponentType<{ className?: string }>;
|
||
|
|
route: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
const kpiTiles: KPITile[] = [
|
||
|
|
{
|
||
|
|
title: 'Total Users',
|
||
|
|
value: '12,847',
|
||
|
|
change: '+12.5%',
|
||
|
|
trend: 'up',
|
||
|
|
icon: Users,
|
||
|
|
route: '/users/individual'
|
||
|
|
},
|
||
|
|
{
|
||
|
|
title: 'Active Learners',
|
||
|
|
value: '8,923',
|
||
|
|
change: '+8.2%',
|
||
|
|
trend: 'up',
|
||
|
|
icon: GraduationCap,
|
||
|
|
route: '/users/individual'
|
||
|
|
},
|
||
|
|
{
|
||
|
|
title: 'Courses Published',
|
||
|
|
value: '156',
|
||
|
|
change: '+3 this week',
|
||
|
|
trend: 'up',
|
||
|
|
icon: BookOpen,
|
||
|
|
route: '/courses'
|
||
|
|
},
|
||
|
|
{
|
||
|
|
title: 'Revenue To-Date',
|
||
|
|
value: '₹2.4M',
|
||
|
|
change: '+18.7%',
|
||
|
|
trend: 'up',
|
||
|
|
icon: DollarSign,
|
||
|
|
route: '/admin/analytics'
|
||
|
|
}
|
||
|
|
];
|
||
|
|
|
||
|
|
const initialAlerts: AlertBanner[] = [
|
||
|
|
{
|
||
|
|
id: '1',
|
||
|
|
type: 'warning',
|
||
|
|
title: 'System Maintenance',
|
||
|
|
message: 'Scheduled maintenance window: Tomorrow 2:00 AM - 4:00 AM IST. Some services may be temporarily unavailable.',
|
||
|
|
dismissible: true
|
||
|
|
},
|
||
|
|
{
|
||
|
|
id: '2',
|
||
|
|
type: 'info',
|
||
|
|
title: 'New Feature Available',
|
||
|
|
message: 'The enhanced analytics dashboard is now live with improved reporting capabilities.',
|
||
|
|
dismissible: true
|
||
|
|
}
|
||
|
|
];
|
||
|
|
|
||
|
|
const taskQueues: TaskQueue[] = [
|
||
|
|
{ title: 'Content Approvals', count: 23, route: '/content', urgent: true },
|
||
|
|
{ title: 'Role-Grant Requests', count: 7, route: '/admin/roles' },
|
||
|
|
{ title: 'Testimonials', count: 15, route: '/content' },
|
||
|
|
{ title: 'Open-Programme Interest', count: 31, route: '/open-programme', urgent: true }
|
||
|
|
];
|
||
|
|
|
||
|
|
const systemHealth = {
|
||
|
|
uptime: '99.8%',
|
||
|
|
queueLength: 12,
|
||
|
|
errorRate: '0.02%'
|
||
|
|
};
|
||
|
|
|
||
|
|
const recentActivities: ActivityItem[] = [
|
||
|
|
{
|
||
|
|
id: '1',
|
||
|
|
type: 'course',
|
||
|
|
user: 'Dr. Sarah Kumar',
|
||
|
|
action: 'published',
|
||
|
|
target: 'Leadership Fundamentals Course',
|
||
|
|
timestamp: '2 minutes ago'
|
||
|
|
},
|
||
|
|
{
|
||
|
|
id: '2',
|
||
|
|
type: 'user',
|
||
|
|
user: 'Admin',
|
||
|
|
action: 'approved',
|
||
|
|
target: 'Tech Corp organization registration',
|
||
|
|
timestamp: '15 minutes ago'
|
||
|
|
},
|
||
|
|
{
|
||
|
|
id: '3',
|
||
|
|
type: 'content',
|
||
|
|
user: 'Prof. Rajesh Mehta',
|
||
|
|
action: 'submitted',
|
||
|
|
target: 'Strategic Planning article for review',
|
||
|
|
timestamp: '1 hour ago'
|
||
|
|
},
|
||
|
|
{
|
||
|
|
id: '4',
|
||
|
|
type: 'programme',
|
||
|
|
user: 'Admin',
|
||
|
|
action: 'scheduled',
|
||
|
|
target: 'Executive Leadership Programme - Batch 2024',
|
||
|
|
timestamp: '2 hours ago'
|
||
|
|
},
|
||
|
|
{
|
||
|
|
id: '5',
|
||
|
|
type: 'webinar',
|
||
|
|
user: 'Dr. Maya Patel',
|
||
|
|
action: 'registered',
|
||
|
|
target: '150 participants for Digital Transformation webinar',
|
||
|
|
timestamp: '3 hours ago'
|
||
|
|
}
|
||
|
|
];
|
||
|
|
|
||
|
|
const quickActions: QuickAction[] = [
|
||
|
|
{
|
||
|
|
title: 'Create Course',
|
||
|
|
description: 'Build a new course with the course builder',
|
||
|
|
icon: Plus,
|
||
|
|
route: '/courses/new'
|
||
|
|
},
|
||
|
|
{
|
||
|
|
title: 'Upload Asset',
|
||
|
|
description: 'Add new content to the library',
|
||
|
|
icon: Upload,
|
||
|
|
route: '/content'
|
||
|
|
},
|
||
|
|
{
|
||
|
|
title: 'Create Landing Page',
|
||
|
|
description: 'Design a new landing page',
|
||
|
|
icon: Globe,
|
||
|
|
route: '/landing-pages'
|
||
|
|
},
|
||
|
|
{
|
||
|
|
title: 'Add HR Org',
|
||
|
|
description: 'Register a new organization',
|
||
|
|
icon: Building,
|
||
|
|
route: '/users/organizations/new'
|
||
|
|
}
|
||
|
|
];
|
||
|
|
|
||
|
|
const executiveTiles = [
|
||
|
|
{
|
||
|
|
title: 'Lead→Enrolment Conversion',
|
||
|
|
value7d: '23.4%',
|
||
|
|
value30d: '28.7%',
|
||
|
|
trend: 'up' as const
|
||
|
|
},
|
||
|
|
{
|
||
|
|
title: 'Programme Fill-Rate',
|
||
|
|
value7d: '87.2%',
|
||
|
|
value30d: '84.1%',
|
||
|
|
trend: 'up' as const
|
||
|
|
},
|
||
|
|
{
|
||
|
|
title: 'Org Activation Rate',
|
||
|
|
value7d: '76.8%',
|
||
|
|
value30d: '71.3%',
|
||
|
|
trend: 'up' as const
|
||
|
|
},
|
||
|
|
{
|
||
|
|
title: '7-Day Revenue Trend',
|
||
|
|
value7d: '₹180K',
|
||
|
|
value30d: '₹720K',
|
||
|
|
trend: 'up' as const
|
||
|
|
}
|
||
|
|
];
|
||
|
|
|
||
|
|
export function Dashboard({ onLogout, user }: DashboardProps) {
|
||
|
|
const [alerts, setAlerts] = useState<AlertBanner[]>(initialAlerts);
|
||
|
|
const [showMoreActivities, setShowMoreActivities] = useState(false);
|
||
|
|
|
||
|
|
const dismissAlert = (alertId: string) => {
|
||
|
|
setAlerts(alerts.filter(alert => alert.id !== alertId));
|
||
|
|
};
|
||
|
|
|
||
|
|
const exportData = (format: 'csv' | 'xlsx' | 'pdf', title: string) => {
|
||
|
|
// Simulate export functionality
|
||
|
|
console.log(`Exporting ${title} as ${format.toUpperCase()}`);
|
||
|
|
};
|
||
|
|
|
||
|
|
const breadcrumbs = [{ label: 'Dashboard' }];
|
||
|
|
const navigate = useNavigate();
|
||
|
|
return (
|
||
|
|
<AuthenticatedLayout
|
||
|
|
currentRoute="/dashboard"
|
||
|
|
// onNavigate={onNavigate}
|
||
|
|
onLogout={onLogout}
|
||
|
|
user={user}
|
||
|
|
breadcrumbs={breadcrumbs}
|
||
|
|
>
|
||
|
|
<div className="p-6 space-y-6 max-w-[1440px] mx-auto">
|
||
|
|
{/* Global KPI Tiles */}
|
||
|
|
<section aria-labelledby="kpi-section">
|
||
|
|
<h2 id="kpi-section" className="sr-only">Key Performance Indicators</h2>
|
||
|
|
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-6">
|
||
|
|
{kpiTiles.map((tile, index) => {
|
||
|
|
const Icon = tile.icon;
|
||
|
|
return (
|
||
|
|
<Card key={index} className="cursor-pointer hover:shadow-md transition-shadow">
|
||
|
|
<CardContent className="p-6" onClick={() => navigate(tile.route)}>
|
||
|
|
<div className="flex items-center justify-between">
|
||
|
|
<div>
|
||
|
|
<p className="text-sm text-muted-foreground">{tile.title}</p>
|
||
|
|
<p className="text-2xl font-semibold">{tile.value}</p>
|
||
|
|
<div className="flex items-center mt-1">
|
||
|
|
<TrendingUp className={`h-4 w-4 mr-1 ${
|
||
|
|
tile.trend === 'up' ? 'text-green-500' :
|
||
|
|
tile.trend === 'down' ? 'text-red-500' :
|
||
|
|
'text-gray-500'
|
||
|
|
}`} />
|
||
|
|
<span className={`text-sm ${
|
||
|
|
tile.trend === 'up' ? 'text-green-600' :
|
||
|
|
tile.trend === 'down' ? 'text-red-600' :
|
||
|
|
'text-gray-600'
|
||
|
|
}`}>
|
||
|
|
{tile.change}
|
||
|
|
</span>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<Icon className="h-8 w-8 text-muted-foreground" />
|
||
|
|
</div>
|
||
|
|
</CardContent>
|
||
|
|
</Card>
|
||
|
|
);
|
||
|
|
})}
|
||
|
|
</div>
|
||
|
|
</section>
|
||
|
|
|
||
|
|
{/* Real-Time Alert Banners */}
|
||
|
|
{alerts.length > 0 && (
|
||
|
|
<section aria-labelledby="alerts-section">
|
||
|
|
<h2 id="alerts-section" className="sr-only">System Alerts</h2>
|
||
|
|
<div className="space-y-3">
|
||
|
|
{alerts.map((alert) => (
|
||
|
|
<Alert key={alert.id} variant={alert.type === 'error' ? 'destructive' : 'default'}>
|
||
|
|
<AlertTriangle className="h-4 w-4" />
|
||
|
|
<AlertDescription className="flex items-center justify-between">
|
||
|
|
<div>
|
||
|
|
<strong>{alert.title}:</strong> {alert.message}
|
||
|
|
</div>
|
||
|
|
{alert.dismissible && (
|
||
|
|
<Button
|
||
|
|
variant="ghost"
|
||
|
|
size="sm"
|
||
|
|
onClick={() => dismissAlert(alert.id)}
|
||
|
|
className="ml-4 h-6 w-6 p-0"
|
||
|
|
>
|
||
|
|
<X className="h-4 w-4" />
|
||
|
|
</Button>
|
||
|
|
)}
|
||
|
|
</AlertDescription>
|
||
|
|
</Alert>
|
||
|
|
))}
|
||
|
|
</div>
|
||
|
|
</section>
|
||
|
|
)}
|
||
|
|
|
||
|
|
<div className="grid grid-cols-1 xl:grid-cols-3 gap-6">
|
||
|
|
{/* Task Queues */}
|
||
|
|
<section aria-labelledby="tasks-section" className="xl:col-span-2">
|
||
|
|
<Card>
|
||
|
|
<CardHeader>
|
||
|
|
<CardTitle id="tasks-section">Task Queues</CardTitle>
|
||
|
|
</CardHeader>
|
||
|
|
<CardContent>
|
||
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||
|
|
{taskQueues.map((queue, index) => (
|
||
|
|
<div
|
||
|
|
key={index}
|
||
|
|
className="flex items-center justify-between p-4 border rounded-lg cursor-pointer hover:bg-accent transition-colors"
|
||
|
|
onClick={() => navigate(queue.route)}
|
||
|
|
>
|
||
|
|
<div>
|
||
|
|
<h4 className="font-medium">{queue.title}</h4>
|
||
|
|
<div className="flex items-center mt-1">
|
||
|
|
<span className="text-2xl font-semibold mr-2">{queue.count}</span>
|
||
|
|
{queue.urgent && (
|
||
|
|
<Badge variant="destructive" className="text-xs">Urgent</Badge>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<Eye className="h-5 w-5 text-muted-foreground" />
|
||
|
|
</div>
|
||
|
|
))}
|
||
|
|
</div>
|
||
|
|
</CardContent>
|
||
|
|
</Card>
|
||
|
|
</section>
|
||
|
|
|
||
|
|
{/* System Health */}
|
||
|
|
<section aria-labelledby="health-section">
|
||
|
|
<Card>
|
||
|
|
<CardHeader>
|
||
|
|
<CardTitle id="health-section">System Health</CardTitle>
|
||
|
|
</CardHeader>
|
||
|
|
<CardContent className="space-y-4">
|
||
|
|
<div>
|
||
|
|
<div className="flex justify-between text-sm mb-1">
|
||
|
|
<span>Uptime</span>
|
||
|
|
<span className="text-green-600 font-medium">{systemHealth.uptime}</span>
|
||
|
|
</div>
|
||
|
|
<Progress value={99.8} className="h-2" />
|
||
|
|
</div>
|
||
|
|
<div>
|
||
|
|
<div className="flex justify-between text-sm">
|
||
|
|
<span>Queue Length</span>
|
||
|
|
<span className="font-medium">{systemHealth.queueLength}</span>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div>
|
||
|
|
<div className="flex justify-between text-sm">
|
||
|
|
<span>Error Rate</span>
|
||
|
|
<span className="text-green-600 font-medium">{systemHealth.errorRate}</span>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div className="pt-2 border-t">
|
||
|
|
<div className="flex items-center text-sm text-green-600">
|
||
|
|
<CheckCircle className="h-4 w-4 mr-2" />
|
||
|
|
All systems operational
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</CardContent>
|
||
|
|
</Card>
|
||
|
|
</section>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="grid grid-cols-1 xl:grid-cols-2 gap-6">
|
||
|
|
{/* Recent Activity Feed */}
|
||
|
|
<section aria-labelledby="activity-section">
|
||
|
|
<Card>
|
||
|
|
<CardHeader>
|
||
|
|
<CardTitle id="activity-section">Recent Activity</CardTitle>
|
||
|
|
</CardHeader>
|
||
|
|
<CardContent>
|
||
|
|
<div className="space-y-4">
|
||
|
|
{recentActivities.slice(0, showMoreActivities ? recentActivities.length : 5).map((activity) => (
|
||
|
|
<div key={activity.id} className="flex items-start space-x-3 pb-3 last:pb-0 border-b last:border-0">
|
||
|
|
<div className="flex-shrink-0 mt-0.5">
|
||
|
|
<div className="h-2 w-2 bg-blue-500 rounded-full"></div>
|
||
|
|
</div>
|
||
|
|
<div className="flex-1 min-w-0">
|
||
|
|
<p className="text-sm">
|
||
|
|
<span className="font-medium">{activity.user}</span>
|
||
|
|
{' '}{activity.action}{' '}
|
||
|
|
<span className="font-medium">{activity.target}</span>
|
||
|
|
</p>
|
||
|
|
<div className="flex items-center mt-1">
|
||
|
|
<Clock className="h-3 w-3 text-muted-foreground mr-1" />
|
||
|
|
<span className="text-xs text-muted-foreground">{activity.timestamp}</span>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
))}
|
||
|
|
</div>
|
||
|
|
{recentActivities.length > 5 && (
|
||
|
|
<Button
|
||
|
|
variant="ghost"
|
||
|
|
onClick={() => setShowMoreActivities(!showMoreActivities)}
|
||
|
|
className="w-full mt-4"
|
||
|
|
>
|
||
|
|
{showMoreActivities ? 'Show Less' : `Show ${recentActivities.length - 5} More`}
|
||
|
|
</Button>
|
||
|
|
)}
|
||
|
|
</CardContent>
|
||
|
|
</Card>
|
||
|
|
</section>
|
||
|
|
|
||
|
|
{/* Quick Actions */}
|
||
|
|
<section aria-labelledby="actions-section">
|
||
|
|
<Card>
|
||
|
|
<CardHeader>
|
||
|
|
<CardTitle id="actions-section">Quick Actions</CardTitle>
|
||
|
|
</CardHeader>
|
||
|
|
<CardContent>
|
||
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||
|
|
{quickActions.map((action, index) => {
|
||
|
|
const Icon = action.icon;
|
||
|
|
return (
|
||
|
|
<Button
|
||
|
|
key={index}
|
||
|
|
variant="outline"
|
||
|
|
className="h-auto p-4 flex flex-col items-start text-left hover:bg-accent transition-colors"
|
||
|
|
onClick={() => navigate(action.route)}
|
||
|
|
>
|
||
|
|
<Icon className="h-5 w-5 mb-2" style={{ color: 'var(--color-brand-primary)' }} />
|
||
|
|
<div>
|
||
|
|
<div className="font-medium">{action.title}</div>
|
||
|
|
<div className="text-xs text-muted-foreground mt-1">{action.description}</div>
|
||
|
|
</div>
|
||
|
|
</Button>
|
||
|
|
);
|
||
|
|
})}
|
||
|
|
</div>
|
||
|
|
</CardContent>
|
||
|
|
</Card>
|
||
|
|
</section>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Executive Insight Tiles */}
|
||
|
|
<section aria-labelledby="insights-section">
|
||
|
|
<Card>
|
||
|
|
<CardHeader>
|
||
|
|
<CardTitle id="insights-section">Executive Insights</CardTitle>
|
||
|
|
</CardHeader>
|
||
|
|
<CardContent>
|
||
|
|
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-6">
|
||
|
|
{executiveTiles.map((tile, index) => (
|
||
|
|
<div key={index} className="space-y-2">
|
||
|
|
<h4 className="font-medium text-sm">{tile.title}</h4>
|
||
|
|
<div className="space-y-1">
|
||
|
|
<div className="flex items-center justify-between">
|
||
|
|
<span className="text-xs text-muted-foreground">7-Day</span>
|
||
|
|
<span className="font-semibold">{tile.value7d}</span>
|
||
|
|
</div>
|
||
|
|
<div className="flex items-center justify-between">
|
||
|
|
<span className="text-xs text-muted-foreground">30-Day</span>
|
||
|
|
<span className="font-semibold">{tile.value30d}</span>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div className="flex items-center justify-between pt-2 border-t">
|
||
|
|
<DropdownMenu>
|
||
|
|
<DropdownMenuTrigger asChild>
|
||
|
|
<Button variant="outline" size="sm">
|
||
|
|
<Download className="h-3 w-3 mr-1" />
|
||
|
|
Export
|
||
|
|
</Button>
|
||
|
|
</DropdownMenuTrigger>
|
||
|
|
<DropdownMenuContent>
|
||
|
|
<DropdownMenuItem onClick={() => exportData('csv', tile.title)}>
|
||
|
|
Export CSV
|
||
|
|
</DropdownMenuItem>
|
||
|
|
<DropdownMenuItem onClick={() => exportData('xlsx', tile.title)}>
|
||
|
|
Export XLSX
|
||
|
|
</DropdownMenuItem>
|
||
|
|
<DropdownMenuItem onClick={() => exportData('pdf', tile.title)}>
|
||
|
|
Export PDF
|
||
|
|
</DropdownMenuItem>
|
||
|
|
</DropdownMenuContent>
|
||
|
|
</DropdownMenu>
|
||
|
|
<Button
|
||
|
|
variant="ghost"
|
||
|
|
size="sm"
|
||
|
|
onClick={() => navigate('/admin/analytics')}
|
||
|
|
>
|
||
|
|
<ExternalLink className="h-3 w-3 mr-1" />
|
||
|
|
Open in Analytics
|
||
|
|
</Button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
))}
|
||
|
|
</div>
|
||
|
|
</CardContent>
|
||
|
|
</Card>
|
||
|
|
</section>
|
||
|
|
</div>
|
||
|
|
</AuthenticatedLayout>
|
||
|
|
);
|
||
|
|
}
|