577 lines
18 KiB
TypeScript
577 lines
18 KiB
TypeScript
import React, { 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 { Input } from '../ui/input';
|
|
import {
|
|
Table,
|
|
TableBody,
|
|
TableCell,
|
|
TableHead,
|
|
TableHeader,
|
|
TableRow,
|
|
} from '../ui/table';
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from '../ui/select';
|
|
import {
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuItem,
|
|
DropdownMenuTrigger,
|
|
} from '../ui/dropdown-menu';
|
|
import { Progress } from '../ui/progress';
|
|
import { toast } from "sonner@2.0.3";
|
|
import {
|
|
Plus,
|
|
Upload,
|
|
Target,
|
|
Folder,
|
|
Building,
|
|
Users,
|
|
Globe,
|
|
BarChart3,
|
|
ExternalLink,
|
|
MoreHorizontal,
|
|
Eye,
|
|
ChevronLeft,
|
|
ChevronRight,
|
|
Calendar,
|
|
Filter,
|
|
TrendingUp,
|
|
TrendingDown,
|
|
Minus,
|
|
GraduationCap,
|
|
BookOpen,
|
|
Award
|
|
} from 'lucide-react';
|
|
import {
|
|
dashboardKPIs,
|
|
mockUsers,
|
|
mockCourses,
|
|
mockProgrammes,
|
|
mockContent,
|
|
mockLeads
|
|
} from '../../data/mockData';
|
|
|
|
interface DashboardProps {
|
|
onNavigate: (route: string) => void;
|
|
onLogout: () => void;
|
|
user: any;
|
|
}
|
|
|
|
// Quick Actions - exactly as specified
|
|
const quickActions = [
|
|
{
|
|
title: "Create Course",
|
|
route: "/courses/new",
|
|
icon: Plus,
|
|
isPrimary: true
|
|
},
|
|
{
|
|
title: "Upload Content",
|
|
route: "/content",
|
|
icon: Upload,
|
|
isPrimary: false
|
|
},
|
|
{
|
|
title: "New Profiler",
|
|
route: "/profilers/new",
|
|
icon: Target,
|
|
isPrimary: false
|
|
},
|
|
{
|
|
title: "Profiler Preview",
|
|
route: "/profilers/preview",
|
|
icon: Eye,
|
|
isPrimary: false
|
|
},
|
|
{
|
|
title: "Create Programme",
|
|
route: "/programmes/new",
|
|
icon: Folder,
|
|
isPrimary: false
|
|
},
|
|
{
|
|
title: "Add Organization",
|
|
route: "/users/organizations/new",
|
|
icon: Building,
|
|
isPrimary: false
|
|
},
|
|
{
|
|
title: "Invite Learners",
|
|
route: "/users/individual",
|
|
icon: Users,
|
|
isPrimary: false
|
|
},
|
|
{
|
|
title: "Create 360 Tour",
|
|
route: "/facilities-360/new",
|
|
icon: Building,
|
|
isPrimary: false
|
|
},
|
|
{
|
|
title: "Open Analytics",
|
|
route: "/admin/analytics",
|
|
icon: BarChart3,
|
|
isPrimary: false
|
|
}
|
|
];
|
|
|
|
// Enhanced KPI Cards with real data
|
|
const getKPICards = () => [
|
|
{
|
|
title: "Live Organizations",
|
|
route: "/users/organizations",
|
|
value: mockUsers.filter(u => u.organization).length.toString(),
|
|
trend: 12.5,
|
|
icon: Building,
|
|
description: "Active organizational clients"
|
|
},
|
|
{
|
|
title: "Live Learners",
|
|
route: "/users/individual",
|
|
value: mockUsers.length.toString(),
|
|
trend: 8.2,
|
|
icon: Users,
|
|
description: "Total enrolled learners"
|
|
},
|
|
{
|
|
title: "Live Programmes",
|
|
route: "/programmes",
|
|
value: mockProgrammes.filter(p => p.status === 'active').length.toString(),
|
|
trend: 15.3,
|
|
icon: Award,
|
|
description: "Currently running programmes"
|
|
}
|
|
];
|
|
|
|
// Mock programme details data
|
|
const mockProgrammeDetails = [
|
|
{
|
|
id: 1,
|
|
programme: "Executive Leadership Program",
|
|
organization: "Tech Corp India",
|
|
users: 45,
|
|
hrAdmin: "Priya Patel",
|
|
klcAdmin: "Dr. Rajesh Mehta",
|
|
status: "Active",
|
|
startDate: "2024-02-01",
|
|
endDate: "2024-07-31",
|
|
progress: 65
|
|
},
|
|
{
|
|
id: 2,
|
|
programme: "Digital Innovation Diploma",
|
|
organization: "Innovation Hub",
|
|
users: 32,
|
|
hrAdmin: "Rohit Gupta",
|
|
klcAdmin: "Prof. Sunita Agarwal",
|
|
status: "Upcoming",
|
|
startDate: "2024-08-01",
|
|
endDate: "2025-07-31",
|
|
progress: 0
|
|
},
|
|
{
|
|
id: 3,
|
|
programme: "Strategic Management Course",
|
|
organization: "Delhi University",
|
|
users: 78,
|
|
hrAdmin: "Ananya Singh",
|
|
klcAdmin: "Dr. Amit Sharma",
|
|
status: "Active",
|
|
startDate: "2024-01-15",
|
|
endDate: "2024-06-15",
|
|
progress: 85
|
|
}
|
|
];
|
|
|
|
export function Dashboard({ onNavigate, onLogout, user }: DashboardProps) {
|
|
const [programmeFilter, setProgrammeFilter] = useState("all");
|
|
const [statusFilter, setStatusFilter] = useState("all");
|
|
const [dateRangeFrom, setDateRangeFrom] = useState("");
|
|
const [dateRangeTo, setDateRangeTo] = useState("");
|
|
|
|
const breadcrumbs = [
|
|
{ label: "Dashboard" }
|
|
];
|
|
|
|
const kpiCards = getKPICards();
|
|
|
|
const filteredProgrammeDetails = mockProgrammeDetails.filter(programme => {
|
|
const matchesStatus = statusFilter === "all" || programme.status.toLowerCase() === statusFilter;
|
|
const matchesProgramme = programmeFilter === "all" ||
|
|
programme.programme.toLowerCase().includes(programmeFilter.toLowerCase());
|
|
return matchesStatus && matchesProgramme;
|
|
});
|
|
|
|
const handleApplyFilters = () => {
|
|
toast.success("Filters applied successfully", {
|
|
duration: 2000
|
|
});
|
|
};
|
|
|
|
const handleResetFilters = () => {
|
|
setProgrammeFilter("all");
|
|
setStatusFilter("all");
|
|
setDateRangeFrom("");
|
|
setDateRangeTo("");
|
|
toast.success("Filters reset to defaults", {
|
|
duration: 2000
|
|
});
|
|
};
|
|
|
|
const handleViewDetails = (route: string, title: string) => {
|
|
onNavigate(route);
|
|
};
|
|
|
|
const getStatusBadgeVariant = (status: string) => {
|
|
switch (status.toLowerCase()) {
|
|
case "active": return "default";
|
|
case "upcoming": return "secondary";
|
|
case "completed": return "outline";
|
|
default: return "secondary";
|
|
}
|
|
};
|
|
|
|
const getTrendIcon = (trend: number) => {
|
|
if (trend > 0) return <TrendingUp className="h-4 w-4 text-green-500" />;
|
|
if (trend < 0) return <TrendingDown className="h-4 w-4 text-red-500" />;
|
|
return <Minus className="h-4 w-4 text-muted-foreground" />;
|
|
};
|
|
|
|
const renderQuickActions = () => (
|
|
<section aria-labelledby="quick-actions-section" className="space-y-4">
|
|
<h2 id="quick-actions-section" className="sr-only">Quick Actions</h2>
|
|
<div className="flex flex-wrap gap-3">
|
|
{quickActions.map((action, index) => {
|
|
const Icon = action.icon;
|
|
const buttonLabels = [
|
|
"Create Course",
|
|
"Upload Content",
|
|
"Create Profiler",
|
|
"Preview Profiler",
|
|
"Create Programme",
|
|
"Add Organization",
|
|
"Add Individual",
|
|
"Create 360 Tour",
|
|
"View Analytics"
|
|
];
|
|
return (
|
|
<Button
|
|
key={index}
|
|
onClick={() => onNavigate(action.route)}
|
|
className={`min-h-[44px] focus-visible:ring-2 focus-visible:ring-[var(--color-brand-primary)] focus-visible:ring-opacity-50 ${
|
|
action.isPrimary
|
|
? ''
|
|
: 'bg-secondary text-secondary-foreground hover:bg-secondary/80'
|
|
}`}
|
|
style={action.isPrimary ? { backgroundColor: "var(--color-brand-primary)" } : {}}
|
|
variant={action.isPrimary ? "default" : "secondary"}
|
|
>
|
|
<Icon className="h-4 w-4 mr-2" />
|
|
{buttonLabels[index] || action.label}
|
|
</Button>
|
|
);
|
|
})}
|
|
</div>
|
|
</section>
|
|
);
|
|
|
|
const renderKPICards = () => (
|
|
<section aria-labelledby="kpi-section" className="space-y-4">
|
|
<h2 id="kpi-section" className="sr-only">Key Performance Indicators</h2>
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
|
{kpiCards.map((kpi, index) => {
|
|
const Icon = kpi.icon;
|
|
return (
|
|
<Card key={index} className="w-full">
|
|
<CardContent className="p-6">
|
|
<div className="flex items-start justify-between">
|
|
<div className="space-y-2 flex-1">
|
|
<div className="flex items-center gap-2">
|
|
<Icon className="h-5 w-5 text-muted-foreground" />
|
|
<h3 className="text-sm font-medium text-foreground">{kpi.title}</h3>
|
|
</div>
|
|
<div className="text-3xl font-bold text-foreground">{kpi.value}</div>
|
|
<div className="flex items-center gap-2">
|
|
{getTrendIcon(kpi.trend)}
|
|
<span className={`text-sm ${kpi.trend > 0 ? 'text-green-600' : kpi.trend < 0 ? 'text-red-600' : 'text-muted-foreground'}`}>
|
|
{kpi.trend > 0 ? '+' : ''}{kpi.trend}% vs last month
|
|
</span>
|
|
</div>
|
|
<p className="text-xs text-muted-foreground">{kpi.description}</p>
|
|
</div>
|
|
<div
|
|
className="w-1 h-16 rounded"
|
|
style={{ backgroundColor: "var(--color-brand-primary)" }}
|
|
/>
|
|
</div>
|
|
<div className="mt-4">
|
|
<Button
|
|
variant="ghost"
|
|
className="h-auto p-0 text-sm hover:underline focus-visible:ring-2 focus-visible:ring-[var(--color-brand-primary)] focus-visible:ring-opacity-50"
|
|
style={{ color: "var(--color-brand-primary)" }}
|
|
onClick={() => handleViewDetails(kpi.route, kpi.title)}
|
|
>
|
|
View details
|
|
<ExternalLink className="h-3 w-3 ml-1" />
|
|
</Button>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
})}
|
|
</div>
|
|
</section>
|
|
);
|
|
|
|
const renderActivitySummary = () => (
|
|
<section aria-labelledby="activity-summary-section" className="space-y-4">
|
|
<h2 id="activity-summary-section" className="sr-only">Activity Summary</h2>
|
|
|
|
</section>
|
|
);
|
|
|
|
const renderProgrammeDetails = () => (
|
|
<section aria-labelledby="programme-details-section" className="space-y-4">
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle id="programme-details-section">Programme-wise details</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="space-y-6">
|
|
{/* Filters Row */}
|
|
<div className="flex flex-wrap items-center gap-4 justify-end">
|
|
<div className="flex items-center gap-2">
|
|
<Select value={programmeFilter} onValueChange={setProgrammeFilter}>
|
|
<SelectTrigger className="w-[140px] min-h-[44px]">
|
|
<SelectValue placeholder="Programme" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="all">All Programmes</SelectItem>
|
|
<SelectItem value="leadership">Leadership</SelectItem>
|
|
<SelectItem value="management">Management</SelectItem>
|
|
<SelectItem value="digital">Digital</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
|
|
<Select value={statusFilter} onValueChange={setStatusFilter}>
|
|
<SelectTrigger className="w-[120px] min-h-[44px]">
|
|
<SelectValue placeholder="Status" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="all">All Status</SelectItem>
|
|
<SelectItem value="active">Active</SelectItem>
|
|
<SelectItem value="completed">Completed</SelectItem>
|
|
<SelectItem value="upcoming">Upcoming</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
|
|
<div className="flex items-center gap-2">
|
|
<Input
|
|
type="date"
|
|
value={dateRangeFrom}
|
|
onChange={(e) => setDateRangeFrom(e.target.value)}
|
|
className="w-[140px] min-h-[44px]"
|
|
placeholder="From"
|
|
/>
|
|
<span className="text-muted-foreground">to</span>
|
|
<Input
|
|
type="date"
|
|
value={dateRangeTo}
|
|
onChange={(e) => setDateRangeTo(e.target.value)}
|
|
className="w-[140px] min-h-[44px]"
|
|
placeholder="To"
|
|
/>
|
|
</div>
|
|
|
|
<Button
|
|
onClick={handleApplyFilters}
|
|
className="min-h-[44px]"
|
|
style={{ backgroundColor: "var(--color-brand-primary)" }}
|
|
>
|
|
Apply
|
|
</Button>
|
|
|
|
<Button
|
|
variant="outline"
|
|
onClick={handleResetFilters}
|
|
className="min-h-[44px]"
|
|
>
|
|
Reset
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Table */}
|
|
<div className="border rounded-lg overflow-hidden">
|
|
<Table>
|
|
<TableHeader className="bg-muted/50 sticky top-0">
|
|
<TableRow>
|
|
<TableHead className="min-w-[200px]">Programme</TableHead>
|
|
<TableHead>Organization</TableHead>
|
|
<TableHead>No. of Users</TableHead>
|
|
<TableHead>HR Admin</TableHead>
|
|
<TableHead>KLC Admin</TableHead>
|
|
<TableHead>Programme Status</TableHead>
|
|
<TableHead>Start Date</TableHead>
|
|
<TableHead>End Date</TableHead>
|
|
<TableHead>Progress</TableHead>
|
|
<TableHead className="w-12">Actions</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{filteredProgrammeDetails.length === 0 ? (
|
|
<TableRow>
|
|
<TableCell colSpan={10} className="text-center py-8 text-muted-foreground">
|
|
No programme data for the selected filters.
|
|
</TableCell>
|
|
</TableRow>
|
|
) : (
|
|
filteredProgrammeDetails.map((programme) => (
|
|
<TableRow key={programme.id}>
|
|
<TableCell>
|
|
<div>
|
|
<p className="font-medium">{programme.programme}</p>
|
|
</div>
|
|
</TableCell>
|
|
<TableCell>{programme.organization}</TableCell>
|
|
<TableCell>
|
|
<div className="flex items-center gap-2">
|
|
<Users className="h-4 w-4 text-muted-foreground" />
|
|
{programme.users}
|
|
</div>
|
|
</TableCell>
|
|
<TableCell>{programme.hrAdmin}</TableCell>
|
|
<TableCell>{programme.klcAdmin}</TableCell>
|
|
<TableCell>
|
|
<Badge variant={getStatusBadgeVariant(programme.status)}>
|
|
{programme.status}
|
|
</Badge>
|
|
</TableCell>
|
|
<TableCell className="text-sm">{programme.startDate}</TableCell>
|
|
<TableCell className="text-sm">{programme.endDate}</TableCell>
|
|
<TableCell>
|
|
<div className="flex items-center gap-2">
|
|
<Progress value={programme.progress} className="w-16" />
|
|
<span className="text-xs text-muted-foreground">{programme.progress}%</span>
|
|
</div>
|
|
</TableCell>
|
|
<TableCell>
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<Button variant="ghost" size="sm" className="h-8 w-8 p-0">
|
|
<MoreHorizontal className="h-4 w-4" />
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align="end">
|
|
<DropdownMenuItem onClick={() => onNavigate('/programmes')}>
|
|
<Eye className="h-4 w-4 mr-2" />
|
|
View Details
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem onClick={() => onNavigate('/admin/analytics')}>
|
|
<BarChart3 className="h-4 w-4 mr-2" />
|
|
Analytics
|
|
</DropdownMenuItem>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
</TableCell>
|
|
</TableRow>
|
|
))
|
|
)}
|
|
</TableBody>
|
|
</Table>
|
|
</div>
|
|
|
|
{/* Footer with pagination */}
|
|
<div className="flex items-center justify-between">
|
|
<div className="text-sm text-muted-foreground">
|
|
Showing {filteredProgrammeDetails.length} of {mockProgrammeDetails.length} results
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
disabled
|
|
className="min-h-[44px] w-[44px] p-0"
|
|
>
|
|
<ChevronLeft className="h-4 w-4" />
|
|
<span className="sr-only">Previous page</span>
|
|
</Button>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
disabled
|
|
className="min-h-[44px] w-[44px] p-0"
|
|
>
|
|
<ChevronRight className="h-4 w-4" />
|
|
<span className="sr-only">Next page</span>
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</section>
|
|
);
|
|
|
|
return (
|
|
<AuthenticatedLayout
|
|
currentRoute="/dashboard"
|
|
onNavigate={onNavigate}
|
|
onLogout={onLogout}
|
|
user={user}
|
|
breadcrumbs={breadcrumbs}
|
|
>
|
|
<div className="p-6 space-y-8 max-w-[1440px] mx-auto">
|
|
{/* Header Row */}
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-3">
|
|
<h1>Dashboard</h1>
|
|
{user.role === "Super Admin" && (
|
|
<Badge
|
|
variant="outline"
|
|
className="text-xs"
|
|
style={{
|
|
borderColor: "var(--color-brand-primary)",
|
|
color: "var(--color-brand-primary)"
|
|
}}
|
|
>
|
|
Super Admin
|
|
</Badge>
|
|
)}
|
|
</div>
|
|
<div className="text-sm text-muted-foregrounde">
|
|
Last updated: {new Date().toLocaleString()}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Quick Actions */}
|
|
{renderQuickActions()}
|
|
|
|
{/* KPI Strip */}
|
|
{renderKPICards()}
|
|
|
|
{/* Activity Summary */}
|
|
{renderActivitySummary()}
|
|
|
|
{/* Programme-wise Details */}
|
|
{renderProgrammeDetails()}
|
|
|
|
{/* Toast area for system messages */}
|
|
<div
|
|
role="status"
|
|
aria-live="polite"
|
|
aria-label="System status messages"
|
|
className="sr-only"
|
|
>
|
|
{/* Toast messages will appear here via the toast system */}
|
|
</div>
|
|
</div>
|
|
</AuthenticatedLayout>
|
|
);
|
|
} |