need to fix layout

This commit is contained in:
priyanshuvish
2026-03-19 13:44:16 +05:30
parent 9823bf9a9e
commit 399b860077
24 changed files with 4617 additions and 6192 deletions

58
package-lock.json generated
View File

@@ -48,6 +48,7 @@
"react-dom": "^18.3.1",
"react-hook-form": "^7.55.0",
"react-resizable-panels": "^2.1.7",
"react-router-dom": "^7.13.1",
"recharts": "^2.15.2",
"sonner": "^2.0.3",
"tailwind-merge": "*",
@@ -2835,6 +2836,19 @@
"react-dom": "^18 || ^19 || ^19.0.0-rc"
}
},
"node_modules/cookie": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz",
"integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==",
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/csstype": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
@@ -3669,6 +3683,44 @@
"react-dom": "^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
}
},
"node_modules/react-router": {
"version": "7.13.1",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.13.1.tgz",
"integrity": "sha512-td+xP4X2/6BJvZoX6xw++A2DdEi++YypA69bJUV5oVvqf6/9/9nNlD70YO1e9d3MyamJEBQFEzk6mbfDYbqrSA==",
"license": "MIT",
"dependencies": {
"cookie": "^1.0.1",
"set-cookie-parser": "^2.6.0"
},
"engines": {
"node": ">=20.0.0"
},
"peerDependencies": {
"react": ">=18",
"react-dom": ">=18"
},
"peerDependenciesMeta": {
"react-dom": {
"optional": true
}
}
},
"node_modules/react-router-dom": {
"version": "7.13.1",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.13.1.tgz",
"integrity": "sha512-UJnV3Rxc5TgUPJt2KJpo1Jpy0OKQr0AjgbZzBFjaPJcFOb2Y8jA5H3LT8HUJAiRLlWrEXWHbF1Z4SCZaQjWDHw==",
"license": "MIT",
"dependencies": {
"react-router": "7.13.1"
},
"engines": {
"node": ">=20.0.0"
},
"peerDependencies": {
"react": ">=18",
"react-dom": ">=18"
}
},
"node_modules/react-smooth": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.4.tgz",
@@ -3803,6 +3855,12 @@
"loose-envify": "^1.1.0"
}
},
"node_modules/set-cookie-parser": {
"version": "2.7.2",
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz",
"integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==",
"license": "MIT"
},
"node_modules/sonner": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.7.tgz",

View File

@@ -43,6 +43,7 @@
"react-dom": "^18.3.1",
"react-hook-form": "^7.55.0",
"react-resizable-panels": "^2.1.7",
"react-router-dom": "^7.13.1",
"recharts": "^2.15.2",
"sonner": "^2.0.3",
"tailwind-merge": "*",

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,79 @@
import React from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
import { ChevronRight, Home } from 'lucide-react';
export const BreadcrumbNav: React.FC = () => {
const location = useLocation();
const navigate = useNavigate();
const pathSegments = location.pathname.split('/').filter(Boolean);
const getDisplayName = (segment: string): string => {
const names: Record<string, string> = {
'hr': 'HR Portal',
'dashboard': 'Dashboard',
'learners': 'Learners',
'reports': 'Reports',
'discussions': 'Discussion Forums',
'programme': 'Programme',
'course': 'Course',
'profile': 'Profile',
'settings': 'Settings'
};
// Handle dynamic segments (like programme IDs)
if (segment.match(/^[0-9a-f-]+$/)) {
return 'Details';
}
return names[segment] || segment.charAt(0).toUpperCase() + segment.slice(1).replace(/-/g, ' ');
};
if (pathSegments.length === 0) return null;
return (
<nav className="flex items-center space-x-2 text-sm mb-6" aria-label="Breadcrumb">
<ol className="flex items-center flex-wrap gap-1">
{/* Home icon */}
<li>
<button
onClick={() => navigate('/hr/dashboard')}
className="text-muted-foreground hover:text-foreground transition-colors p-1"
aria-label="Go to dashboard"
>
<Home className="h-4 w-4" />
</button>
</li>
{pathSegments.map((segment, index) => {
const isLast = index === pathSegments.length - 1;
const path = '/' + pathSegments.slice(0, index + 1).join('/');
const displayName = getDisplayName(segment);
return (
<li key={path} className="flex items-center">
<ChevronRight className="h-4 w-4 text-muted-foreground mx-1" aria-hidden="true" />
{isLast ? (
<span
className="font-medium text-foreground"
aria-current="page"
>
{displayName}
</span>
) : (
<button
onClick={() => navigate(path)}
className="text-muted-foreground hover:text-foreground transition-colors"
>
{displayName}
</button>
)}
</li>
);
})}
</ol>
</nav>
);
};
export default BreadcrumbNav;

308
src/components/TopNav.tsx Normal file
View File

@@ -0,0 +1,308 @@
import React from 'react';
import { useNavigate } from 'react-router-dom';
import klcLogo from '../assets/klc-logo.png';
import {
Menu,
Bell,
User,
Settings,
LogOut,
Building2,
BookOpen,
Sun,
Moon,
HelpCircle,
ChevronDown
} from 'lucide-react';
import { Button } from './ui/button';
import { Badge } from './ui/badge';
import { Avatar, AvatarFallback } from './ui/avatar';
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger } from './ui/dropdown-menu';
interface TopNavProps {
onMenuToggle?: () => void;
showMenuButton?: boolean;
onNotificationToggle?: () => void;
notificationCount?: number;
}
interface UserPreferences {
darkMode: boolean;
prefersReducedMotion: boolean;
}
// Custom hook for localStorage
const useLocalStorage = <T,>(key: string, initialValue: T): [T, (value: T) => void] => {
const [storedValue, setStoredValue] = React.useState<T>(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
return initialValue;
}
});
const setValue = (value: T) => {
try {
setStoredValue(value);
window.localStorage.setItem(key, JSON.stringify(value));
} catch (error) {
console.error('Error saving to localStorage:', error);
}
};
return [storedValue, setValue];
};
export const TopNav: React.FC<TopNavProps> = ({
onMenuToggle,
showMenuButton = false,
onNotificationToggle,
notificationCount = 0
}) => {
const navigate = useNavigate();
const [preferences, setPreferences] = useLocalStorage<UserPreferences>('userPreferences', {
darkMode: false,
prefersReducedMotion: false
});
const toggleDarkMode = () => {
const newDarkMode = !preferences.darkMode;
setPreferences({
...preferences,
darkMode: newDarkMode
});
document.documentElement.classList.toggle('dark', newDarkMode);
};
const handleSignOut = () => {
// Add your sign out logic here
console.log('Signing out...');
// Clear any auth tokens/user data
localStorage.removeItem('authToken');
sessionStorage.clear();
navigate('/login');
};
const handleProfileClick = () => {
navigate('/hr/profile');
};
const handleSettingsClick = () => {
navigate('/hr/settings');
};
const handleHelpClick = () => {
window.open('/help', '_blank');
};
const handleSwitchMode = (mode: 'hr' | 'learning') => {
if (mode === 'learning') {
navigate('/learning/dashboard');
} else {
navigate('/hr/dashboard');
}
};
return (
<header className="h-16 bg-background border-b border-chrome-divider flex items-center justify-between px-4 lg:px-6 sticky top-0 z-30">
{/* Left Section */}
<div className="flex items-center gap-4">
{showMenuButton && (
<Button
variant="ghost"
size="icon"
onClick={onMenuToggle}
className="lg:hidden min-tap-44"
aria-label="Toggle navigation menu"
aria-expanded="false"
>
<Menu className="h-5 w-5" />
</Button>
)}
{/* Logo and Brand */}
<div className="flex items-center gap-3">
<img
src={klcLogo}
alt="Kautilya Leadership Centre"
className="h-8 w-auto"
/>
<div className="hidden sm:block">
<h1 className="text-sm font-semibold">HR Dashboard</h1>
<p className="text-xs text-muted-foreground">Knowledge Learning Centre</p>
</div>
</div>
</div>
{/* Right Section */}
<div className="flex items-center gap-2">
{/* Notifications */}
<Button
variant="ghost"
size="icon"
className="min-tap-44 relative"
aria-label="Notifications"
onClick={onNotificationToggle}
>
<Bell className="h-4 w-4" />
{notificationCount > 0 && (
<Badge
variant="destructive"
className="absolute -top-1 -right-1 h-5 w-5 p-0 flex items-center justify-center text-xs"
aria-label={`${notificationCount} unread notifications`}
>
{notificationCount > 9 ? '9+' : notificationCount}
</Badge>
)}
</Button>
{/* Theme Toggle */}
<Button
variant="ghost"
size="icon"
onClick={toggleDarkMode}
className="min-tap-44 hidden sm:flex"
aria-label={preferences.darkMode ? 'Switch to light mode' : 'Switch to dark mode'}
>
{preferences.darkMode ? <Sun className="h-4 w-4" /> : <Moon className="h-4 w-4" />}
</Button>
{/* Profile Dropdown */}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
className="flex items-center gap-2 px-2 min-tap-44 hover:bg-accent"
aria-label="User menu"
>
<Avatar className="h-8 w-8">
<AvatarFallback className="bg-brand-navy text-white">
HR
</AvatarFallback>
</Avatar>
<div className="hidden md:block text-left">
<p className="text-sm font-medium">HR Manager</p>
<p className="text-xs text-muted-foreground">hr@klc.com</p>
</div>
<ChevronDown className="h-4 w-4 text-muted-foreground hidden sm:block" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-80">
{/* User Profile Section */}
<div className="flex items-center gap-3 p-4 bg-muted/20">
<Avatar className="h-12 w-12">
<AvatarFallback className="bg-brand-navy text-white text-lg">
HR
</AvatarFallback>
</Avatar>
<div>
<p className="font-semibold">HR Manager</p>
<p className="text-sm text-muted-foreground">hr@klc.com</p>
<Badge variant="outline" className="mt-1 text-xs">
Administrator
</Badge>
</div>
</div>
{/* Quick Stats */}
<div className="grid grid-cols-2 gap-2 p-3 border-b">
<div className="text-center p-2 bg-muted/30 rounded">
<p className="text-xs text-muted-foreground">Active Learners</p>
<p className="text-lg font-semibold">247</p>
</div>
<div className="text-center p-2 bg-muted/30 rounded">
<p className="text-xs text-muted-foreground">Programmes</p>
<p className="text-lg font-semibold">12</p>
</div>
</div>
{/* Switch Mode Section */}
<div className="p-3 border-b">
<p className="text-xs font-medium text-muted-foreground mb-2">SWITCH MODE</p>
<div className="flex gap-2">
<Button
size="sm"
variant="outline"
className="flex-1"
onClick={() => handleSwitchMode('learning')}
>
<BookOpen className="mr-2 h-4 w-4" />
Learning
</Button>
<Button
size="sm"
className="flex-1 bg-brand-navy text-white hover:bg-brand-navy/90"
onClick={() => handleSwitchMode('hr')}
>
<Building2 className="mr-2 h-4 w-4" />
HR Mode
</Button>
</div>
</div>
{/* Menu Items */}
<div className="p-2">
<DropdownMenuItem onClick={handleProfileClick} className="p-3 cursor-pointer">
<User className="mr-3 h-4 w-4" />
<div>
<p className="font-medium">My Profile</p>
<p className="text-xs text-muted-foreground">View and edit your profile</p>
</div>
</DropdownMenuItem>
<DropdownMenuItem onClick={handleSettingsClick} className="p-3 cursor-pointer">
<Settings className="mr-3 h-4 w-4" />
<div>
<p className="font-medium">Settings</p>
<p className="text-xs text-muted-foreground">Manage your preferences</p>
</div>
</DropdownMenuItem>
<DropdownMenuItem onClick={handleHelpClick} className="p-3 cursor-pointer">
<HelpCircle className="mr-3 h-4 w-4" />
<div>
<p className="font-medium">Help & Support</p>
<p className="text-xs text-muted-foreground">Get help and documentation</p>
</div>
</DropdownMenuItem>
</div>
<DropdownMenuSeparator />
{/* Sign out */}
<DropdownMenuItem
onClick={handleSignOut}
className="p-3 text-status-error focus:bg-status-error/10 focus:text-status-error cursor-pointer"
>
<LogOut className="mr-3 h-4 w-4" />
<div>
<p className="font-medium">Sign out</p>
<p className="text-xs text-muted-foreground">End your session</p>
</div>
</DropdownMenuItem>
{/* Footer */}
<div className="p-3 text-center text-xs text-muted-foreground border-t">
<p>Version 2.0.0</p>
</div>
</DropdownMenuContent>
</DropdownMenu>
{/* Mobile Theme Toggle (visible only on small screens) */}
<Button
variant="ghost"
size="icon"
onClick={toggleDarkMode}
className="min-tap-44 sm:hidden"
aria-label={preferences.darkMode ? 'Switch to light mode' : 'Switch to dark mode'}
>
{preferences.darkMode ? <Sun className="h-4 w-4" /> : <Moon className="h-4 w-4" />}
</Button>
</div>
</header>
);
};
export default TopNav;

View File

@@ -0,0 +1,69 @@
import React, { useState } from 'react';
import { Button } from '../ui/button';
import { MessageSquare, X } from 'lucide-react';
interface ChatBotProps {
currentScreen?: string;
}
export const ChatBot: React.FC<ChatBotProps> = ({ currentScreen }) => {
const [isOpen, setIsOpen] = useState(false);
const getChipsForScreen = (screen?: string) => {
if (screen === 'profile') {
return [
"How do I submit a testimonial?",
"When will my testimonial be reviewed?",
"Can I edit my testimonial?",
"What makes a good testimonial?"
];
}
return [
"How do I upload a roster?",
"How to assign courses?",
"View progress reports",
"Export learner data"
];
};
const chips = getChipsForScreen(currentScreen);
return (
<div className="fixed bottom-6 right-6 z-40">
{isOpen && (
<div className="mb-4 bg-card border border-chrome-divider rounded-lg shadow-lg p-4 w-80 animate-slide-up">
<div className="flex justify-between items-center mb-3">
<h3 className="font-semibold">HR Assistant</h3>
<Button
variant="ghost"
size="icon"
onClick={() => setIsOpen(false)}
className="h-6 w-6"
aria-label="Close chat"
>
<X className="h-4 w-4" />
</Button>
</div>
<div className="space-y-2">
{chips.map((chip, index) => (
<button
key={index}
className="w-full text-left p-2 text-sm bg-muted hover:bg-accent rounded-md transition-colors min-tap-44"
>
{chip}
</button>
))}
</div>
</div>
)}
<Button
onClick={() => setIsOpen(!isOpen)}
className="rounded-full h-12 w-12 shadow-lg min-tap-44"
aria-label="Open HR chat assistant"
aria-expanded={isOpen}
>
<MessageSquare className="h-5 w-5" />
</Button>
</div>
);
};

View File

@@ -0,0 +1,84 @@
import React from 'react';
import { Button } from '../ui/button';
import { Badge } from '../ui/badge';
import { X, Bell, Clock } from 'lucide-react';
import { Announcement } from '../../types';
interface AnnouncementsPanelProps {
isOpen: boolean;
onClose: () => void;
announcements: Announcement[];
onMarkAsRead?: (id: string) => void;
}
export const AnnouncementsPanel: React.FC<AnnouncementsPanelProps> = ({
isOpen,
onClose,
announcements,
onMarkAsRead
}) => {
if (!isOpen) return null;
return (
<div className="fixed inset-y-0 right-0 w-80 bg-background border-l border-chrome-divider shadow-lg z-50 flex flex-col">
<div className="flex items-center justify-between p-4 border-b border-chrome-divider">
<h3 className="font-semibold">Announcements & Reminders</h3>
<Button
variant="ghost"
size="icon"
onClick={onClose}
className="h-8 w-8"
aria-label="Close announcements panel"
>
<X className="h-4 w-4" />
</Button>
</div>
<div className="flex-1 overflow-y-auto p-4 space-y-3">
{announcements.map((item) => (
<div
key={item.id}
className={`
p-3 rounded-lg border transition-all duration-200 cursor-pointer hover:bg-muted/50
${item.pinned ? 'bg-status-warn/10 border-status-warn/20' : 'bg-card border-chrome-divider'}
`}
onClick={() => onMarkAsRead?.(item.id)}
>
<div className="flex items-start justify-between mb-2">
<div className="flex items-center gap-2">
<div className={`p-1 rounded ${item.type === 'announcement' ? 'bg-brand-primary' : 'bg-status-warn'}`}>
{item.type === 'announcement' ?
<Bell className="h-3 w-3 text-white" /> :
<Clock className="h-3 w-3 text-white" />
}
</div>
{item.pinned && (
<Badge variant="secondary" className="text-xs">Pinned</Badge>
)}
</div>
<span className="text-xs text-muted-foreground">{item.timestamp}</span>
</div>
<h4 className="font-medium text-sm mb-1">{item.title}</h4>
<p className="text-xs text-muted-foreground">{item.content}</p>
<div className="flex items-center justify-between mt-2">
<Badge variant="outline" className="text-xs capitalize">
{item.type}
</Badge>
<Button variant="ghost" size="sm" className="h-auto p-1 text-xs">
Mark as read
</Button>
</div>
</div>
))}
</div>
<div className="p-4 border-t border-chrome-divider">
<Button variant="outline" className="w-full text-sm">
View All Notifications
</Button>
</div>
</div>
);
};

View File

@@ -1,7 +1,7 @@
import * as React from "react";
import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu@1.2.5";
import { cva } from "class-variance-authority@0.7.1";
import { ChevronDownIcon } from "lucide-react@0.487.0";
import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu";
import { cva } from "class-variance-authority";
import { ChevronDownIcon } from "lucide-react";
import { cn } from "./utils";

23
src/hooks/useCountUp.ts Normal file
View File

@@ -0,0 +1,23 @@
import { useState, useEffect } from 'react';
export function useCountUp(end: number, duration: number = 1200) {
const [count, setCount] = useState(0);
useEffect(() => {
let start = 0;
const increment = end / (duration / 16);
const timer = setInterval(() => {
start += increment;
if (start >= end) {
setCount(end);
clearInterval(timer);
} else {
setCount(Math.floor(start));
}
}, 16);
return () => clearInterval(timer);
}, [end, duration]);
return count;
}

View File

@@ -0,0 +1,24 @@
import { useState, useEffect } from 'react';
export function useLocalStorage<T>(key: string, initialValue: T): [T, (value: T) => void] {
const [storedValue, setStoredValue] = useState<T>(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.error('Error reading from localStorage:', error);
return initialValue;
}
});
const setValue = (value: T) => {
try {
setStoredValue(value);
window.localStorage.setItem(key, JSON.stringify(value));
} catch (error) {
console.error('Error saving to localStorage:', error);
}
};
return [storedValue, setValue];
}

105
src/layouts/HRLayout.tsx Normal file
View File

@@ -0,0 +1,105 @@
import React, { useState, useEffect } from 'react';
import { Outlet } from 'react-router-dom';
import { BreadcrumbNav } from './components/BreadcrumbNav';
import { ChatBot } from '../components/shared/ChatBot';
import { mockAnnouncements } from '../utils/mockData';
import { useLocalStorage } from '../hooks/useLocalStorage';
import TopNav from '../components/TopNav';
import { HRSidebar } from './components/HRSidebar';
import { AnnouncementsPanel } from '../components/shared/KPICard';
const HRLayout: React.FC = () => {
const [sidebarOpen, setSidebarOpen] = useState(false);
const [announcementsOpen, setAnnouncementsOpen] = useState(false);
const [isDark] = useLocalStorage('darkMode', false);
// Apply theme
useEffect(() => {
document.documentElement.classList.toggle('dark', isDark);
}, [isDark]);
const handleNotificationToggle = () => {
setAnnouncementsOpen(!announcementsOpen);
};
const handleMarkAsRead = (id: string) => {
console.log(`Marked notification ${id} as read`);
// In a real app, you would call an API to mark as read
};
return (
<div className="h-screen overflow-hidden bg-background flex flex-col">
{/* Skip to main content link for accessibility */}
<a
href="#main-content"
className="sr-only focus:not-sr-only focus:absolute focus:top-4 focus:left-4 bg-primary text-primary-foreground p-2 rounded z-50"
>
Skip to main content
</a>
{/* Top Navigation */}
<TopNav
onMenuToggle={() => setSidebarOpen(!sidebarOpen)}
showMenuButton={true}
onNotificationToggle={handleNotificationToggle}
notificationCount={mockAnnouncements.length}
/>
<div className="flex flex-1 relative overflow-hidden">
{/* Desktop Sidebar */}
<HRSidebar className="hidden lg:flex lg:flex-shrink-0" />
{/* Mobile Sidebar Overlay */}
{sidebarOpen && (
<div className="fixed inset-0 z-50 lg:hidden">
{/* Backdrop */}
<div
className="absolute inset-0 bg-background/80 backdrop-blur-sm"
onClick={() => setSidebarOpen(false)}
aria-hidden="true"
/>
{/* Sidebar */}
<div className="absolute left-0 top-0 h-full">
<HRSidebar
onNavigate={() => setSidebarOpen(false)}
/>
</div>
</div>
)}
{/* Main Content */}
<main
id="main-content"
className={`flex-1 overflow-y-auto p-4 lg:p-8 transition-all duration-300 ${announcementsOpen ? 'lg:mr-80' : ''
}`}
>
<div className="max-w-7xl mx-auto">
<div className="sticky top-0 bg-background z-10 pb-2">
<BreadcrumbNav />
</div>
<Outlet />
</div>
</main>
</div>
{/* Announcements Panel */}
<AnnouncementsPanel
isOpen={announcementsOpen}
onClose={() => setAnnouncementsOpen(false)}
announcements={mockAnnouncements}
onMarkAsRead={handleMarkAsRead}
/>
{/* Chat Bot FAB */}
<ChatBot />
{/* Footer */}
<footer className="bg-muted border-t border-chrome-divider p-4 text-center text-sm text-muted-foreground">
<p>&copy; {new Date().getFullYear()} Knowledge Learning Centre. All rights reserved.</p>
</footer>
</div>
);
};
export default HRLayout;

View File

@@ -0,0 +1,63 @@
import React from 'react';
import { Link, useLocation } from 'react-router-dom';
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
} from '../../components/ui/breadcrumb';
export const BreadcrumbNav: React.FC = () => {
const location = useLocation();
const pathnames = location.pathname.split('/').filter(x => x);
const getBreadcrumbName = (path: string) => {
switch (path) {
case 'hr': return 'HR Portal';
case 'dashboard': return 'Dashboard';
case 'learners': return 'Learners';
case 'reports': return 'Reports';
case 'discussions': return 'Discussion Forums';
case 'programme': return 'Programme Details';
case 'course': return 'Course Details';
case 'profile': return 'Profile';
default: return path;
}
};
return (
<Breadcrumb className="mb-6">
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink asChild>
<Link to="/hr">HR Portal</Link>
</BreadcrumbLink>
</BreadcrumbItem>
{pathnames.map((name, index) => {
const routeTo = `/${pathnames.slice(0, index + 1).join('/')}`;
const isLast = index === pathnames.length - 1;
if (name === 'hr') return null;
return (
<React.Fragment key={name}>
<BreadcrumbSeparator aria-hidden="true" />
<BreadcrumbItem>
{isLast ? (
<BreadcrumbPage>{getBreadcrumbName(name)}</BreadcrumbPage>
) : (
<BreadcrumbLink asChild>
<Link to={routeTo}>{getBreadcrumbName(name)}</Link>
</BreadcrumbLink>
)}
</BreadcrumbItem>
</React.Fragment>
);
})}
</BreadcrumbList>
</Breadcrumb>
);
};

View File

@@ -0,0 +1,69 @@
import React from 'react';
import { NavLink } from 'react-router-dom';
import {
Home,
Users,
BarChart3,
MessageSquare
} from 'lucide-react';
import { Button } from '../../components/ui/button';
import { useLocalStorage } from '../../hooks/useLocalStorage';
const menuItems = [
{ id: 'dashboard', label: 'Dashboard', icon: Home, path: '/hr/dashboard' },
{ id: 'learners', label: 'Learners', icon: Users, path: '/hr/learners' },
{ id: 'reports', label: 'Reports', icon: BarChart3, path: '/hr/reports' },
{ id: 'discussions', label: 'Discussion Forums', icon: MessageSquare, path: '/hr/discussions' }
];
interface HRSidebarProps {
className?: string;
onNavigate?: () => void;
}
export const HRSidebar: React.FC<HRSidebarProps> = ({ className = '', onNavigate }) => {
const [prefersReducedMotion] = useLocalStorage('prefersReducedMotion', false);
return (
<div className={`w-64 min-w-[248px] h-full bg-sidebar flex flex-col ${className}`}>
<div className="p-6">
<div className="flex items-center gap-2">
<div className="w-8 h-8 bg-brand-charcoal rounded-md flex items-center justify-center">
<span className="text-brand-charcoal-foreground font-bold text-sm">TS</span>
</div>
<span className="font-semibold text-sidebar-foreground">Tech Solutions Pvt Ltd</span>
</div>
</div>
<nav className="flex-1 p-4" role="navigation" aria-label="HR Portal Navigation">
<ul className="space-y-1">
{menuItems.map((item) => {
const Icon = item.icon;
return (
<li key={item.id}>
<NavLink
to={item.path}
onClick={onNavigate}
className={({ isActive }) => `
w-full flex items-center gap-3 px-3 py-2 rounded-md text-sm min-tap-44
transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-sidebar-ring focus:ring-offset-2 focus:ring-offset-sidebar
${isActive
? 'bg-sidebar-primary text-sidebar-primary-foreground shadow-sm'
: 'text-sidebar-foreground hover:bg-sidebar-accent hover:text-sidebar-accent-foreground'
}
${prefersReducedMotion ? '' : 'animate-scale-hover'}
`}
aria-label={`Navigate to ${item.label}`}
>
<Icon className="h-4 w-4" />
{item.label}
</NavLink>
</li>
);
})}
</ul>
</nav>
</div>
);
};

View File

@@ -1,7 +1,10 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import './index.css';
import { createRoot } from "react-dom/client";
import App from "./App.tsx";
import "./index.css";
createRoot(document.getElementById("root")!).render(<App />);
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>
);

View File

@@ -0,0 +1,503 @@
import React, { useState, useEffect } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { Button } from '../../components/ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../../components/ui/card';
import { Badge } from '../../components/ui/badge';
import { Progress } from '../../components/ui/progress';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '../../components/ui/tabs';
import { Avatar, AvatarFallback } from '../../components/ui/avatar';
import {
ArrowLeft,
BookOpen,
Clock,
Award,
Download,
Users,
Video,
FileText,
CheckCircle,
AlertCircle,
PlayCircle,
File,
Link as LinkIcon,
MessageSquare,
Star,
ThumbsUp,
Share2,
Bookmark,
ChevronRight,
Menu
} from 'lucide-react';
interface Course {
id: string;
title: string;
programmeId: string;
programmeName: string;
description: string;
instructor: {
name: string;
title: string;
avatar?: string;
};
duration: string;
enrolledCount: number;
rating: number;
progress: number;
status: 'Not Started' | 'In Progress' | 'Completed';
modules: CourseModule[];
resources: Resource[];
discussions: Discussion[];
}
interface CourseModule {
id: string;
title: string;
duration: string;
type: 'video' | 'reading' | 'quiz' | 'assignment';
status: 'locked' | 'available' | 'completed';
content?: string;
videoUrl?: string;
}
interface Resource {
id: string;
title: string;
type: 'pdf' | 'video' | 'link' | 'document';
url: string;
}
interface Discussion {
id: string;
user: {
name: string;
avatar?: string;
};
content: string;
timestamp: string;
likes: number;
replies: number;
}
// Mock data
const mockCourse: Course = {
id: '1',
title: 'Strategic Thinking for Leaders',
programmeId: 'p1',
programmeName: 'Leadership Development Program',
description: 'Learn how to develop strategic thinking capabilities, analyze complex business situations, and make decisions that drive organizational success.',
instructor: {
name: 'Prof. Michael Chen',
title: 'Senior Leadership Coach',
},
duration: '4 weeks',
enrolledCount: 42,
rating: 4.8,
progress: 65,
status: 'In Progress',
modules: [
{
id: 'm1',
title: 'Introduction to Strategic Thinking',
duration: '45 min',
type: 'video',
status: 'completed',
videoUrl: '#'
},
{
id: 'm2',
title: 'Strategic Analysis Frameworks',
duration: '60 min',
type: 'video',
status: 'completed',
videoUrl: '#'
},
{
id: 'm3',
title: 'Decision Making Models',
duration: '90 min',
type: 'reading',
status: 'available'
},
{
id: 'm4',
title: 'Case Study Analysis',
duration: '120 min',
type: 'assignment',
status: 'available'
},
{
id: 'm5',
title: 'Strategic Planning Quiz',
duration: '30 min',
type: 'quiz',
status: 'locked'
}
],
resources: [
{ id: 'r1', title: 'Strategic Analysis Template', type: 'document', url: '#' },
{ id: 'r2', title: 'Decision Matrix Worksheet', type: 'pdf', url: '#' },
{ id: 'r3', title: 'Case Study Materials', type: 'pdf', url: '#' }
],
discussions: [
{
id: 'd1',
user: { name: 'Sarah Chen' },
content: 'The SWOT analysis framework was really helpful. I\'ve already started applying it to my projects.',
timestamp: '2 hours ago',
likes: 12,
replies: 3
},
{
id: 'd2',
user: { name: 'David Kim' },
content: 'Can someone explain the difference between strategic and operational decisions?',
timestamp: '5 hours ago',
likes: 5,
replies: 8
}
]
};
const CourseViewPage: React.FC = () => {
const { id } = useParams<{ id: string }>();
const navigate = useNavigate();
const [course, setCourse] = useState<Course | null>(null);
const [loading, setLoading] = useState(true);
const [activeTab, setActiveTab] = useState('overview');
const [selectedModule, setSelectedModule] = useState<CourseModule | null>(null);
useEffect(() => {
// Simulate API call
const timer = setTimeout(() => {
setCourse(mockCourse);
setSelectedModule(mockCourse.modules[0]);
setLoading(false);
}, 500);
return () => clearTimeout(timer);
}, [id]);
if (loading) {
return (
<div className="space-y-6">
<div className="h-8 w-64 bg-muted animate-pulse rounded" />
<div className="h-32 bg-muted animate-pulse rounded" />
<div className="h-64 bg-muted animate-pulse rounded" />
</div>
);
}
if (!course) {
return (
<div className="text-center py-12">
<AlertCircle className="h-12 w-12 mx-auto mb-4 text-muted-foreground" />
<h2 className="text-xl font-semibold mb-2">Course not found</h2>
<p className="text-muted-foreground mb-4">The course you're looking for doesn't exist.</p>
<Button onClick={() => navigate('/dashboard')}>Back to Dashboard</Button>
</div>
);
}
const getModuleIcon = (type: string) => {
switch (type) {
case 'video': return <Video className="h-5 w-5" />;
case 'reading': return <FileText className="h-5 w-5" />;
case 'quiz': return <Award className="h-5 w-5" />;
case 'assignment': return <BookOpen className="h-5 w-5" />;
default: return <File className="h-5 w-5" />;
}
};
return (
<div className="space-y-6">
{/* Header */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<Button
variant="ghost"
size="sm"
onClick={() => navigate(`/programme/${course.programmeId}`)}
className="min-tap-44"
>
<ArrowLeft className="h-4 w-4 mr-2" />
Back to Programme
</Button>
<div>
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<span>{course.programmeName}</span>
<ChevronRight className="h-4 w-4" />
<span className="text-foreground">{course.title}</span>
</div>
<h1 className="text-2xl font-bold mt-1">{course.title}</h1>
</div>
</div>
<div className="flex items-center gap-2">
<Button variant="outline" size="sm">
<Bookmark className="h-4 w-4 mr-2" />
Save
</Button>
<Button variant="outline" size="sm">
<Share2 className="h-4 w-4 mr-2" />
Share
</Button>
</div>
</div>
{/* Course Stats */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<Card>
<CardContent className="pt-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-muted-foreground">Your Progress</p>
<p className="text-2xl font-bold">{course.progress}%</p>
</div>
<Progress value={course.progress} className="w-16 h-16" />
</div>
</CardContent>
</Card>
<Card>
<CardContent className="pt-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-muted-foreground">Duration</p>
<p className="text-2xl font-bold">{course.duration}</p>
</div>
<Clock className="h-8 w-8 text-blue-500" />
</div>
</CardContent>
</Card>
<Card>
<CardContent className="pt-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-muted-foreground">Enrolled</p>
<p className="text-2xl font-bold">{course.enrolledCount}</p>
</div>
<Users className="h-8 w-8 text-green-500" />
</div>
</CardContent>
</Card>
<Card>
<CardContent className="pt-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-muted-foreground">Rating</p>
<p className="text-2xl font-bold">{course.rating}/5.0</p>
</div>
<Star className="h-8 w-8 text-yellow-500 fill-current" />
</div>
</CardContent>
</Card>
</div>
{/* Main Content */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Course Content */}
<div className="lg:col-span-2 space-y-6">
{/* Course Info */}
<Card>
<CardHeader>
<CardTitle>About this Course</CardTitle>
</CardHeader>
<CardContent>
<p className="text-muted-foreground">{course.description}</p>
<div className="flex items-center gap-4 mt-4 pt-4 border-t">
<div className="flex items-center gap-3">
<Avatar>
<AvatarFallback>
{course.instructor.name.split(' ').map(n => n[0]).join('')}
</AvatarFallback>
</Avatar>
<div>
<p className="font-medium">{course.instructor.name}</p>
<p className="text-sm text-muted-foreground">{course.instructor.title}</p>
</div>
</div>
<Badge variant="outline">{course.status}</Badge>
</div>
</CardContent>
</Card>
{/* Course Modules */}
<Card>
<CardHeader>
<CardTitle>Course Modules</CardTitle>
<CardDescription>{course.modules.length} modules {course.duration} total</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-3">
{course.modules.map((module, index) => (
<div
key={module.id}
className={`p-4 rounded-lg border cursor-pointer transition-all ${
selectedModule?.id === module.id
? 'border-primary bg-primary/5'
: 'hover:border-primary/50'
}`}
onClick={() => setSelectedModule(module)}
>
<div className="flex items-start gap-3">
<div className={`w-8 h-8 rounded-full flex items-center justify-center ${
module.status === 'completed'
? 'bg-green-100 text-green-600'
: module.status === 'available'
? 'bg-blue-100 text-blue-600'
: 'bg-gray-100 text-gray-400'
}`}>
{module.status === 'completed' ? (
<CheckCircle className="h-5 w-5" />
) : (
getModuleIcon(module.type)
)}
</div>
<div className="flex-1">
<div className="flex items-center justify-between">
<h4 className="font-medium">{module.title}</h4>
<Badge variant={module.status === 'locked' ? 'outline' : 'secondary'}>
{module.status}
</Badge>
</div>
<div className="flex items-center gap-3 mt-1 text-sm text-muted-foreground">
<span className="flex items-center gap-1">
<Clock className="h-3 w-3" />
{module.duration}
</span>
<span></span>
<span className="capitalize">{module.type}</span>
</div>
</div>
</div>
</div>
))}
</div>
</CardContent>
</Card>
{/* Discussions Preview */}
<Card>
<CardHeader>
<CardTitle>Recent Discussions</CardTitle>
<CardDescription>Join the conversation</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-4">
{course.discussions.map(discussion => (
<div key={discussion.id} className="border-b last:border-0 pb-4 last:pb-0">
<div className="flex items-start gap-3">
<Avatar className="w-8 h-8">
<AvatarFallback>
{discussion.user.name.split(' ').map(n => n[0]).join('')}
</AvatarFallback>
</Avatar>
<div className="flex-1">
<div className="flex items-center justify-between mb-1">
<span className="font-medium">{discussion.user.name}</span>
<span className="text-xs text-muted-foreground">
{discussion.timestamp}
</span>
</div>
<p className="text-sm mb-2">{discussion.content}</p>
<div className="flex items-center gap-4 text-xs text-muted-foreground">
<button className="flex items-center gap-1 hover:text-foreground">
<ThumbsUp className="h-3 w-3" />
{discussion.likes}
</button>
<button className="flex items-center gap-1 hover:text-foreground">
<MessageSquare className="h-3 w-3" />
{discussion.replies} replies
</button>
</div>
</div>
</div>
</div>
))}
</div>
</CardContent>
</Card>
</div>
{/* Current Module View */}
<div className="lg:col-span-1">
<Card className="sticky top-6">
<CardHeader>
<CardTitle>Current Module</CardTitle>
<CardDescription>
{selectedModule?.title}
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
{selectedModule && (
<>
<div className="aspect-video bg-muted rounded-lg flex items-center justify-center">
{selectedModule.type === 'video' ? (
<PlayCircle className="h-12 w-12 text-muted-foreground" />
) : (
<FileText className="h-12 w-12 text-muted-foreground" />
)}
</div>
<div className="space-y-2">
<div className="flex justify-between text-sm">
<span className="text-muted-foreground">Type:</span>
<span className="capitalize">{selectedModule.type}</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-muted-foreground">Duration:</span>
<span>{selectedModule.duration}</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-muted-foreground">Status:</span>
<Badge variant={
selectedModule.status === 'completed' ? 'default' :
selectedModule.status === 'available' ? 'secondary' : 'outline'
}>
{selectedModule.status}
</Badge>
</div>
</div>
<Button
className="w-full"
disabled={selectedModule.status === 'locked'}
>
{selectedModule.status === 'completed' ? 'Review Module' :
selectedModule.status === 'available' ? 'Start Module' :
'Locked'}
</Button>
{/* Resources */}
<div className="pt-4 border-t">
<h4 className="font-medium mb-3">Resources</h4>
<div className="space-y-2">
{course.resources.map(resource => (
<a
key={resource.id}
href={resource.url}
className="flex items-center gap-2 p-2 rounded-lg hover:bg-muted transition-colors"
>
{resource.type === 'pdf' && <FileText className="h-4 w-4" />}
{resource.type === 'video' && <Video className="h-4 w-4" />}
{resource.type === 'link' && <LinkIcon className="h-4 w-4" />}
{resource.type === 'document' && <File className="h-4 w-4" />}
<span className="text-sm flex-1">{resource.title}</span>
<Download className="h-4 w-4 text-muted-foreground" />
</a>
))}
</div>
</div>
</>
)}
</CardContent>
</Card>
</div>
</div>
</div>
);
};
export default CourseViewPage;

View File

@@ -0,0 +1,164 @@
import React, { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../../components/ui/card';
import { ProgrammesTable } from '../../components/ProgrammesTable';
import { ProgrammeSchedule } from '../../components/ProgrammeSchedule';
import { LearningAnalyticsTable } from '../../components/LearningAnalyticsTable';
import { DiscussionForumFeed } from '../../components/DiscussionForumFeed';
import { Skeleton } from '../../components/ui/skeleton';
import { Plus, BookOpen, Download, MessageSquare } from 'lucide-react';
import { mockKPIData } from '../../utils/mockData';
import { useLocalStorage } from '../../hooks/useLocalStorage';
import { AnnouncementsPanel } from '../../components/shared/KPICard';
const DashboardPage: React.FC = () => {
const navigate = useNavigate();
const [loading, setLoading] = useState(true);
const [prefersReducedMotion] = useLocalStorage('prefersReducedMotion', false);
useEffect(() => {
const timer = setTimeout(() => setLoading(false), 800);
return () => clearTimeout(timer);
}, []);
// Helper function to determine user access level
const getUserAccessLevel = (): 'full' | 'course-only' => {
return 'full'; // Default to full access
};
const handleViewProgramme = (programmeId: string) => {
navigate(`/hr/programme/${programmeId}`);
};
const handleViewCourse = (courseId: string) => {
navigate(`/hr/course/${courseId}`);
};
const handleNavigate = (path: string, params?: any) => {
navigate(path);
};
if (loading) {
return (
<div className="space-y-6">
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
{[...Array(4)].map((_, i) => (
<Card key={i}>
<CardHeader className="pb-2">
<Skeleton className="h-4 w-24" />
</CardHeader>
<CardContent>
<Skeleton className="h-8 w-16" />
</CardContent>
</Card>
))}
</div>
<Card>
<CardHeader>
<Skeleton className="h-6 w-48" />
</CardHeader>
<CardContent>
<Skeleton className="h-64 w-full" />
</CardContent>
</Card>
</div>
);
}
return (
<div className="space-y-6">
{/* Welcome Section */}
<div className={`space-y-4 ${prefersReducedMotion ? '' : 'animate-fade-in'}`}>
<div className="space-y-2">
<h1>Welcome Priya 👋</h1>
<p className="text-muted-foreground">Manage programmes, track progress, and stay connected with your learning community</p>
</div>
</div>
{/* KPI Cards */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
{mockKPIData.map((kpi, index) => (
<AnnouncementsPanel
key={index}
data={kpi}
onClick={() => handleNavigate('/hr/reports')}
className={prefersReducedMotion ? '' : 'animate-slide-up'}
style={{ animationDelay: `${index * 100}ms` }}
/>
))}
</div>
{/* Quick Actions Section */}
<Card className={prefersReducedMotion ? '' : 'animate-slide-up'} style={{ animationDelay: '200ms' }}>
<CardHeader>
<CardTitle>Quick Actions</CardTitle>
<CardDescription>Common HR tasks and shortcuts</CardDescription>
</CardHeader>
<CardContent>
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
{[
{ title: 'Add Learners', icon: Plus, action: () => handleNavigate('/hr/learners', { action: 'add' }) },
{ title: 'Assign', icon: BookOpen, action: () => handleNavigate('/hr/learners', { action: 'assign' }) },
{ title: 'Download Reports', icon: Download, action: () => handleNavigate('/hr/reports') },
{ title: 'Submit Testimonial', icon: MessageSquare, action: () => handleNavigate('/hr/profile') }
].map((link, index) => {
const Icon = link.icon;
return (
<button
key={index}
onClick={link.action}
className={`
flex flex-col items-center justify-center p-6 bg-muted/50 hover:bg-muted rounded-lg
transition-all duration-200 min-h-[120px] min-w-[120px] gap-3 min-tap-44
${prefersReducedMotion ? '' : 'animate-scale-hover'}
`}
aria-label={link.title}
>
<Icon className="h-6 w-6 text-brand-primary" />
<span className="text-sm font-medium text-center">{link.title}</span>
</button>
);
})}
</div>
</CardContent>
</Card>
{/* Programmes Table */}
<div className={prefersReducedMotion ? '' : 'animate-slide-up'} style={{ animationDelay: '400ms' }}>
<ProgrammesTable
onViewProgramme={handleViewProgramme}
onViewCourse={handleViewCourse}
onAssignLearners={(programmeId) => console.log(`Assign learners to: ${programmeId}`)}
onDownloadTracker={(programmeId) => console.log(`Download tracker for: ${programmeId}`)}
userAccessLevel={getUserAccessLevel()}
/>
</div>
{/* Programme Schedule - Horizontal layout below programmes */}
<div className={prefersReducedMotion ? '' : 'animate-slide-up'} style={{ animationDelay: '500ms' }}>
<ProgrammeSchedule
onEventClick={(event) => console.log(`Open event: ${event.title}`)}
/>
</div>
{/* Learning Analytics - Full Width */}
<div className={prefersReducedMotion ? '' : 'animate-slide-up'} style={{ animationDelay: '600ms' }}>
<LearningAnalyticsTable
onViewLearner={(learnerId) => handleNavigate('/hr/learners', { editEmployee: learnerId })}
onNudgeLearner={(learnerId) => console.log(`Nudge learner: ${learnerId}`)}
onViewAllAnalytics={(programmeId) => handleNavigate('/hr/reports', { programme: programmeId })}
/>
</div>
{/* Discussion Forum Feed */}
<div className={prefersReducedMotion ? '' : 'animate-slide-up'} style={{ animationDelay: '800ms' }}>
<DiscussionForumFeed
onOpenThread={(threadId) => handleNavigate('/hr/discussions', { thread: threadId })}
onMarkAsRead={(threadId) => console.log(`Mark as read: ${threadId}`)}
/>
</div>
</div>
);
};
export default DashboardPage;

View File

@@ -0,0 +1,710 @@
import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { Button } from '../../components/ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../../components/ui/card';
import { Input } from '../../components/ui/input';
import { Textarea } from '../../components/ui/textarea';
import { Badge } from '../../components/ui/badge';
import { Avatar, AvatarFallback } from '../../components/ui/avatar';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../../components/ui/select';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '../../components/ui/tabs';
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '../../components/ui/dialog';
import {
MessageSquare,
Plus,
Search,
Filter,
Pin,
Heart,
Reply,
Share2,
Flag,
Eye,
Clock,
Users,
Hash,
ThumbsUp,
MessageCircle,
ChevronRight,
ArrowLeft,
Send,
MoreHorizontal,
Bookmark,
Bell,
CheckCircle,
AlertCircle
} from 'lucide-react';
interface Author {
id: string;
name: string;
avatar?: string;
role?: string;
}
interface Thread {
id: string;
title: string;
content: string;
author: Author;
category: string;
tags: string[];
createdAt: string;
lastActivity: string;
replies: number;
views: number;
likes: number;
isPinned: boolean;
isLocked: boolean;
isSolved: boolean;
}
interface Reply {
id: string;
threadId: string;
content: string;
author: Author;
createdAt: string;
likes: number;
isBestAnswer: boolean;
parentId?: string;
}
// Mock data
const mockThreads: Thread[] = [
{
id: '1',
title: 'Best practices for remote team communication',
content: 'What strategies have you found most effective for maintaining clear communication with remote team members? I\'d love to hear about tools and techniques that have worked well for your teams.',
author: { id: 'u1', name: 'Sarah Chen', role: 'HR Manager' },
category: 'best-practices',
tags: ['communication', 'remote-work', 'leadership'],
createdAt: '2024-12-28T10:30:00Z',
lastActivity: '2024-12-28T15:45:00Z',
replies: 12,
views: 245,
likes: 34,
isPinned: true,
isLocked: false,
isSolved: false
},
{
id: '2',
title: 'How to handle difficult conversations with team members?',
content: 'I\'m struggling with addressing performance issues with one of my team members. Any advice on how to approach this sensitively while being direct about expectations?',
author: { id: 'u2', name: 'Michael Rodriguez', role: 'Team Lead' },
category: 'advice',
tags: ['difficult-conversations', 'performance-management'],
createdAt: '2024-12-28T09:15:00Z',
lastActivity: '2024-12-28T14:20:00Z',
replies: 8,
views: 156,
likes: 21,
isPinned: false,
isLocked: false,
isSolved: true
},
{
id: '3',
title: 'Share your leadership book recommendations',
content: 'What books have been most influential in your leadership journey? Looking for practical reads that offer actionable insights.',
author: { id: 'u3', name: 'Emma Thompson', role: 'Learning Specialist' },
category: 'recommendations',
tags: ['books', 'learning', 'leadership'],
createdAt: '2024-12-27T16:00:00Z',
lastActivity: '2024-12-28T11:30:00Z',
replies: 15,
views: 312,
likes: 45,
isPinned: false,
isLocked: false,
isSolved: false
},
{
id: '4',
title: 'Question about delegation framework from Module 3',
content: 'Can someone clarify the difference between the delegation levels we covered? I want to make sure I\'m applying them correctly in my current projects.',
author: { id: 'u4', name: 'David Kim', role: 'Project Manager' },
category: 'questions',
tags: ['delegation', 'module-3', 'clarification'],
createdAt: '2024-12-27T14:30:00Z',
lastActivity: '2024-12-27T18:45:00Z',
replies: 6,
views: 98,
likes: 12,
isPinned: false,
isLocked: false,
isSolved: false
}
];
const mockReplies: Reply[] = [
{
id: 'r1',
threadId: '1',
content: 'Great question! I\'ve found that establishing clear communication protocols at the start of projects makes a huge difference. We use a combination of daily stand-ups via video call and async updates through Slack.',
author: { id: 'u5', name: 'Lisa Wang', role: 'HR Coordinator' },
createdAt: '2024-12-28T11:00:00Z',
likes: 8,
isBestAnswer: false
},
{
id: 'r2',
threadId: '1',
content: 'One thing that\'s worked well for our team is having "communication preferences" documented for each team member. Some prefer quick calls for complex topics, others prefer detailed written explanations.',
author: { id: 'u6', name: 'Robert Lee', role: 'Team Lead' },
createdAt: '2024-12-28T12:15:00Z',
likes: 12,
isBestAnswer: true
}
];
const DiscussionsPage: React.FC = () => {
const navigate = useNavigate();
const [selectedCategory, setSelectedCategory] = useState('all');
const [searchTerm, setSearchTerm] = useState('');
const [sortBy, setSortBy] = useState('latest');
const [viewMode, setViewMode] = useState<'list' | 'detail'>('list');
const [selectedThread, setSelectedThread] = useState<Thread | null>(null);
const [showNewThreadModal, setShowNewThreadModal] = useState(false);
const [newThread, setNewThread] = useState({ title: '', content: '', category: '', tags: '' });
const [replyContent, setReplyContent] = useState('');
const [bookmarkedThreads, setBookmarkedThreads] = useState<string[]>([]);
const categories = [
{ id: 'all', name: 'All Discussions', count: mockThreads.length },
{ id: 'best-practices', name: 'Best Practices', count: mockThreads.filter(t => t.category === 'best-practices').length },
{ id: 'advice', name: 'Advice', count: mockThreads.filter(t => t.category === 'advice').length },
{ id: 'recommendations', name: 'Recommendations', count: mockThreads.filter(t => t.category === 'recommendations').length },
{ id: 'questions', name: 'Questions', count: mockThreads.filter(t => t.category === 'questions').length }
];
const filteredThreads = mockThreads
.filter(thread => {
const matchesCategory = selectedCategory === 'all' || thread.category === selectedCategory;
const matchesSearch = thread.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
thread.content.toLowerCase().includes(searchTerm.toLowerCase()) ||
thread.tags.some(tag => tag.includes(searchTerm.toLowerCase()));
return matchesCategory && matchesSearch;
})
.sort((a, b) => {
if (sortBy === 'latest') return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime();
if (sortBy === 'popular') return b.views - a.views;
if (sortBy === 'active') return new Date(b.lastActivity).getTime() - new Date(a.lastActivity).getTime();
return 0;
});
const threadReplies = selectedThread ? mockReplies.filter(r => r.threadId === selectedThread.id) : [];
const formatDate = (dateString: string) => {
const date = new Date(dateString);
const now = new Date();
const diffMs = now.getTime() - date.getTime();
const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
const diffDays = Math.floor(diffHours / 24);
if (diffHours < 1) return 'Just now';
if (diffHours < 24) return `${diffHours}h ago`;
if (diffDays < 7) return `${diffDays}d ago`;
return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
};
const handleCreateThread = () => {
console.log('Creating thread:', newThread);
setShowNewThreadModal(false);
setNewThread({ title: '', content: '', category: '', tags: '' });
};
const handleAddReply = () => {
if (replyContent.trim() && selectedThread) {
console.log('Adding reply to thread:', selectedThread.id, replyContent);
setReplyContent('');
}
};
const toggleBookmark = (threadId: string) => {
setBookmarkedThreads(prev =>
prev.includes(threadId) ? prev.filter(id => id !== threadId) : [...prev, threadId]
);
};
return (
<div className="space-y-6">
{/* Header */}
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div>
<h1 className="text-2xl font-bold">Discussion Forums</h1>
<p className="text-muted-foreground">Connect, share, and learn with your peers</p>
</div>
<Button onClick={() => setShowNewThreadModal(true)} className="min-tap-44">
<Plus className="h-4 w-4 mr-2" />
New Discussion
</Button>
</div>
{/* Stats Cards */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<Card>
<CardContent className="pt-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-muted-foreground">Total Discussions</p>
<p className="text-2xl font-bold">{mockThreads.length}</p>
</div>
<MessageSquare className="h-8 w-8 text-blue-500" />
</div>
</CardContent>
</Card>
<Card>
<CardContent className="pt-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-muted-foreground">Active Today</p>
<p className="text-2xl font-bold">
{mockThreads.filter(t => {
const lastActivity = new Date(t.lastActivity);
const today = new Date();
return lastActivity.toDateString() === today.toDateString();
}).length}
</p>
</div>
<Users className="h-8 w-8 text-green-500" />
</div>
</CardContent>
</Card>
<Card>
<CardContent className="pt-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-muted-foreground">Total Replies</p>
<p className="text-2xl font-bold">
{mockThreads.reduce((acc, t) => acc + t.replies, 0)}
</p>
</div>
<Reply className="h-8 w-8 text-purple-500" />
</div>
</CardContent>
</Card>
<Card>
<CardContent className="pt-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-muted-foreground">Solved</p>
<p className="text-2xl font-bold">
{mockThreads.filter(t => t.isSolved).length}
</p>
</div>
<CheckCircle className="h-8 w-8 text-yellow-500" />
</div>
</CardContent>
</Card>
</div>
{viewMode === 'list' ? (
<>
{/* Search and Filters */}
<Card>
<CardContent className="pt-6">
<div className="flex flex-col lg:flex-row gap-4 items-start lg:items-center justify-between">
<div className="flex flex-col sm:flex-row gap-4 items-start sm:items-center flex-1">
<div className="relative flex-1 max-w-md">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
placeholder="Search discussions..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-10"
/>
</div>
<Select value={sortBy} onValueChange={setSortBy}>
<SelectTrigger className="w-[150px]">
<SelectValue placeholder="Sort by" />
</SelectTrigger>
<SelectContent>
<SelectItem value="latest">Latest</SelectItem>
<SelectItem value="active">Most Active</SelectItem>
<SelectItem value="popular">Most Viewed</SelectItem>
</SelectContent>
</Select>
</div>
<div className="flex gap-2">
<Button variant="outline" size="sm" className="min-tap-44">
<Filter className="h-4 w-4 mr-2" />
Filters
</Button>
</div>
</div>
</CardContent>
</Card>
{/* Categories and Threads */}
<div className="grid grid-cols-1 lg:grid-cols-4 gap-6">
{/* Categories Sidebar */}
<Card className="lg:col-span-1">
<CardHeader>
<CardTitle>Categories</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-2">
{categories.map(category => (
<button
key={category.id}
onClick={() => setSelectedCategory(category.id)}
className={`w-full flex items-center justify-between p-2 rounded-lg transition-colors ${
selectedCategory === category.id
? 'bg-primary text-primary-foreground'
: 'hover:bg-muted'
}`}
>
<span>{category.name}</span>
<Badge variant={selectedCategory === category.id ? 'secondary' : 'outline'}>
{category.count}
</Badge>
</button>
))}
</div>
</CardContent>
</Card>
{/* Threads List */}
<div className="lg:col-span-3 space-y-4">
{filteredThreads.map(thread => (
<Card
key={thread.id}
className="cursor-pointer hover:shadow-lg transition-all"
onClick={() => {
setSelectedThread(thread);
setViewMode('detail');
}}
>
<CardContent className="pt-6">
<div className="flex items-start gap-4">
<Avatar className="w-10 h-10">
<AvatarFallback>
{thread.author.name.split(' ').map(n => n[0]).join('')}
</AvatarFallback>
</Avatar>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1">
{thread.isPinned && (
<Pin className="h-4 w-4 text-yellow-500" />
)}
{thread.isSolved && (
<CheckCircle className="h-4 w-4 text-green-500" />
)}
<h3 className="font-semibold truncate">{thread.title}</h3>
</div>
<p className="text-sm text-muted-foreground line-clamp-2 mb-2">
{thread.content}
</p>
<div className="flex flex-wrap items-center gap-2 text-xs text-muted-foreground">
<span className="font-medium text-foreground">
{thread.author.name}
</span>
<span></span>
<span>{thread.author.role}</span>
<span></span>
<span>{formatDate(thread.createdAt)}</span>
</div>
<div className="flex flex-wrap items-center gap-3 mt-3">
{thread.tags.map(tag => (
<Badge key={tag} variant="secondary" className="text-xs">
#{tag}
</Badge>
))}
</div>
</div>
<div className="flex flex-col items-end gap-2 text-sm text-muted-foreground">
<div className="flex items-center gap-3">
<span className="flex items-center gap-1">
<MessageCircle className="h-4 w-4" />
{thread.replies}
</span>
<span className="flex items-center gap-1">
<Eye className="h-4 w-4" />
{thread.views}
</span>
<span className="flex items-center gap-1">
<Heart className="h-4 w-4" />
{thread.likes}
</span>
</div>
<span className="text-xs">
Last activity {formatDate(thread.lastActivity)}
</span>
</div>
</div>
</CardContent>
</Card>
))}
</div>
</div>
</>
) : (
/* Thread Detail View */
selectedThread && (
<div className="space-y-6">
{/* Back Button */}
<Button
variant="ghost"
size="sm"
onClick={() => setViewMode('list')}
className="min-tap-44"
>
<ArrowLeft className="h-4 w-4 mr-2" />
Back to Discussions
</Button>
{/* Main Thread */}
<Card>
<CardContent className="pt-6">
<div className="flex items-start gap-4">
<Avatar className="w-12 h-12">
<AvatarFallback>
{selectedThread.author.name.split(' ').map(n => n[0]).join('')}
</AvatarFallback>
</Avatar>
<div className="flex-1">
<div className="flex items-start justify-between gap-4">
<div>
<div className="flex items-center gap-2 mb-1">
{selectedThread.isPinned && (
<Pin className="h-4 w-4 text-yellow-500" />
)}
{selectedThread.isSolved && (
<CheckCircle className="h-4 w-4 text-green-500" />
)}
<h2 className="text-xl font-semibold">{selectedThread.title}</h2>
</div>
<div className="flex items-center gap-2 text-sm text-muted-foreground mb-4">
<span className="font-medium text-foreground">
{selectedThread.author.name}
</span>
<span></span>
<span>{selectedThread.author.role}</span>
<span></span>
<span>{formatDate(selectedThread.createdAt)}</span>
</div>
</div>
<div className="flex items-center gap-2">
<Button
variant="ghost"
size="sm"
onClick={() => toggleBookmark(selectedThread.id)}
className="min-tap-44"
>
<Bookmark className={`h-4 w-4 ${
bookmarkedThreads.includes(selectedThread.id) ? 'fill-current' : ''
}`} />
</Button>
<Button variant="ghost" size="sm" className="min-tap-44">
<Share2 className="h-4 w-4" />
</Button>
<Button variant="ghost" size="sm" className="min-tap-44">
<Flag className="h-4 w-4" />
</Button>
</div>
</div>
<div className="prose prose-sm max-w-none mt-4">
<p>{selectedThread.content}</p>
</div>
<div className="flex flex-wrap items-center gap-2 mt-4">
{selectedThread.tags.map(tag => (
<Badge key={tag} variant="secondary">
#{tag}
</Badge>
))}
</div>
<div className="flex items-center gap-4 mt-4 pt-4 border-t">
<Button variant="ghost" size="sm" className="min-tap-44">
<Heart className="h-4 w-4 mr-2" />
Like ({selectedThread.likes})
</Button>
<Button variant="ghost" size="sm" className="min-tap-44">
<Reply className="h-4 w-4 mr-2" />
Reply
</Button>
<span className="text-sm text-muted-foreground">
{selectedThread.views} views
</span>
</div>
</div>
</div>
</CardContent>
</Card>
{/* Replies */}
<Card>
<CardHeader>
<CardTitle>Replies ({threadReplies.length})</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-6">
{threadReplies.map(reply => (
<div key={reply.id} className={`p-4 rounded-lg ${
reply.isBestAnswer ? 'bg-green-50 border border-green-200' : 'bg-muted/30'
}`}>
<div className="flex items-start gap-3">
<Avatar className="w-8 h-8">
<AvatarFallback>
{reply.author.name.split(' ').map(n => n[0]).join('')}
</AvatarFallback>
</Avatar>
<div className="flex-1">
<div className="flex items-center justify-between mb-1">
<div className="flex items-center gap-2">
<span className="font-medium">{reply.author.name}</span>
<span className="text-xs text-muted-foreground">
{reply.author.role}
</span>
<span className="text-xs text-muted-foreground">
{formatDate(reply.createdAt)}
</span>
{reply.isBestAnswer && (
<Badge variant="default" className="text-xs">
Best Answer
</Badge>
)}
</div>
<Button variant="ghost" size="sm" className="min-tap-44">
<MoreHorizontal className="h-4 w-4" />
</Button>
</div>
<p className="text-sm">{reply.content}</p>
<div className="flex items-center gap-4 mt-2">
<Button variant="ghost" size="sm" className="h-8 px-2">
<Heart className="h-4 w-4 mr-1" />
{reply.likes}
</Button>
<Button variant="ghost" size="sm" className="h-8 px-2">
<Reply className="h-4 w-4 mr-1" />
Reply
</Button>
</div>
</div>
</div>
</div>
))}
</div>
</CardContent>
</Card>
{/* Add Reply */}
<Card>
<CardHeader>
<CardTitle>Add Reply</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-4">
<Textarea
value={replyContent}
onChange={(e) => setReplyContent(e.target.value)}
placeholder="Share your thoughts or answer the question..."
className="min-h-[100px]"
/>
<div className="flex justify-end">
<Button
onClick={handleAddReply}
disabled={!replyContent.trim()}
className="min-tap-44"
>
<Send className="h-4 w-4 mr-2" />
Post Reply
</Button>
</div>
</div>
</CardContent>
</Card>
</div>
)
)}
{/* New Thread Modal */}
<Dialog open={showNewThreadModal} onOpenChange={setShowNewThreadModal}>
<DialogContent className="sm:max-w-[600px]">
<DialogHeader>
<DialogTitle>Start New Discussion</DialogTitle>
<DialogDescription>
Share your thoughts, ask a question, or start a conversation with the community.
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium mb-2">Title *</label>
<Input
value={newThread.title}
onChange={(e) => setNewThread(prev => ({ ...prev, title: e.target.value }))}
placeholder="What would you like to discuss?"
maxLength={120}
/>
</div>
<div>
<label className="block text-sm font-medium mb-2">Category *</label>
<Select onValueChange={(value) => setNewThread(prev => ({ ...prev, category: value }))}>
<SelectTrigger>
<SelectValue placeholder="Select a category" />
</SelectTrigger>
<SelectContent>
<SelectItem value="best-practices">Best Practices</SelectItem>
<SelectItem value="advice">Advice</SelectItem>
<SelectItem value="recommendations">Recommendations</SelectItem>
<SelectItem value="questions">Questions</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<label className="block text-sm font-medium mb-2">Content *</label>
<Textarea
value={newThread.content}
onChange={(e) => setNewThread(prev => ({ ...prev, content: e.target.value }))}
placeholder="Share your thoughts in detail..."
className="min-h-[150px]"
/>
</div>
<div>
<label className="block text-sm font-medium mb-2">Tags (comma separated)</label>
<Input
value={newThread.tags}
onChange={(e) => setNewThread(prev => ({ ...prev, tags: e.target.value }))}
placeholder="leadership, communication, remote-work"
/>
</div>
<div className="flex gap-2 pt-4">
<Button
onClick={handleCreateThread}
disabled={!newThread.title || !newThread.content || !newThread.category}
className="flex-1"
>
Create Discussion
</Button>
<Button variant="outline" onClick={() => setShowNewThreadModal(false)} className="flex-1">
Cancel
</Button>
</div>
</div>
</DialogContent>
</Dialog>
</div>
);
};
export default DiscussionsPage;

View File

@@ -0,0 +1,823 @@
import React, { useState, useEffect, useCallback } from 'react';
import { useLocation } from 'react-router-dom';
import { Button } from '../../components/ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../../components/ui/card';
import { Input } from '../../components/ui/input';
import { Badge } from '../../components/ui/badge';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../../components/ui/select';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '../../components/ui/table';
import { Checkbox } from '../../components/ui/checkbox';
import { Progress } from '../../components/ui/progress';
import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from '../../components/ui/sheet';
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '../../components/ui/dialog';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '../../components/ui/tabs';
import {
Search,
Plus,
Upload,
Download,
Edit,
MoreHorizontal,
Users,
Eye,
Send,
UserPlus,
RefreshCw,
ChevronLeft,
ChevronRight,
Filter,
Mail,
Phone,
Calendar,
BookOpen,
Award,
AlertCircle,
CheckCircle,
XCircle,
Clock
} from 'lucide-react';
import { mockEmployees } from '../../utils/mockData';
import { Employee } from '../../types';
const LearnersPage: React.FC = () => {
const location = useLocation();
const [employees, setEmployees] = useState<Employee[]>(mockEmployees);
const [searchTerm, setSearchTerm] = useState('');
const [statusFilter, setStatusFilter] = useState('all');
const [programmeFilter, setProgrammeFilter] = useState('all');
const [selectedEmployees, setSelectedEmployees] = useState<string[]>([]);
const [showAddDrawer, setShowAddDrawer] = useState(false);
const [showImportModal, setShowImportModal] = useState(false);
const [showAssignModal, setShowAssignModal] = useState(false);
const [showEditDrawer, setShowEditDrawer] = useState(false);
const [editingEmployee, setEditingEmployee] = useState<Employee | null>(null);
const [newEmployee, setNewEmployee] = useState({ name: '', email: '', phone: '' });
const [bulkActionVisible, setBulkActionVisible] = useState(false);
const [currentPage, setCurrentPage] = useState(1);
const [itemsPerPage, setItemsPerPage] = useState(10);
const [loading, setLoading] = useState(false);
const [viewMode, setViewMode] = useState<'table' | 'grid'>('table');
// Get unique programmes for filter
const programmes = Array.from(new Set(employees.map(e => e.programme).filter(Boolean)));
const debouncedSearch = useCallback(
(term: string) => {
const timer = setTimeout(() => {
setSearchTerm(term);
}, 300);
return () => clearTimeout(timer);
},
[]
);
const filteredEmployees = employees.filter(emp => {
const matchesSearch = emp.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
emp.email.toLowerCase().includes(searchTerm.toLowerCase());
const matchesStatus = statusFilter === 'all' || emp.status === statusFilter;
const matchesProgramme = programmeFilter === 'all' || emp.programme === programmeFilter;
return matchesSearch && matchesStatus && matchesProgramme;
});
// Pagination calculations
const totalPages = Math.ceil(filteredEmployees.length / itemsPerPage);
const startIndex = (currentPage - 1) * itemsPerPage;
const endIndex = startIndex + itemsPerPage;
const paginatedEmployees = filteredEmployees.slice(startIndex, endIndex);
// Reset to first page when filters change
useEffect(() => {
setCurrentPage(1);
}, [searchTerm, statusFilter, programmeFilter]);
useEffect(() => {
setBulkActionVisible(selectedEmployees.length > 0);
}, [selectedEmployees]);
// Check for action from location state
useEffect(() => {
const state = location.state as any;
if (state?.action === 'add') {
setShowAddDrawer(true);
} else if (state?.action === 'assign') {
setShowAssignModal(true);
} else if (state?.editEmployee) {
const employee = employees.find(e => e.id === state.editEmployee);
if (employee) {
setEditingEmployee(employee);
setShowEditDrawer(true);
}
}
}, [location.state, employees]);
const handleEmployeeSelect = (employeeId: string, selected: boolean) => {
if (selected) {
setSelectedEmployees(prev => [...prev, employeeId]);
} else {
setSelectedEmployees(prev => prev.filter(id => id !== employeeId));
}
};
const handleBulkSelect = (selectAll: boolean) => {
if (selectAll) {
setSelectedEmployees(paginatedEmployees.map(emp => emp.id));
} else {
setSelectedEmployees([]);
}
};
const handlePageChange = (page: number) => {
setCurrentPage(page);
setSelectedEmployees([]);
};
const handleAddEmployee = () => {
if (newEmployee.name && newEmployee.email) {
const newEmp: Employee = {
id: Date.now().toString(),
name: newEmployee.name,
email: newEmployee.email,
phone: newEmployee.phone,
status: 'Pending',
progress: 0,
lastActivity: 'Just now'
};
setEmployees(prev => [...prev, newEmp]);
setNewEmployee({ name: '', email: '', phone: '' });
setShowAddDrawer(false);
}
};
const handleEditEmployee = (employee: Employee) => {
setEditingEmployee(employee);
setShowEditDrawer(true);
};
const handleExport = async (format: 'excel' | 'csv' | 'pdf') => {
setLoading(true);
await new Promise(resolve => setTimeout(resolve, 2000));
setLoading(false);
console.log(`Exported learner data as ${format}`);
};
const getStatusColor = (status: string) => {
switch (status) {
case 'Active': return 'default';
case 'Pending': return 'secondary';
case 'Inactive': return 'destructive';
default: return 'default';
}
};
const getProgressColor = (progress: number) => {
if (progress >= 80) return 'bg-green-500';
if (progress >= 50) return 'bg-yellow-500';
return 'bg-red-500';
};
return (
<div className="space-y-6">
{/* Header */}
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div>
<h1 className="text-2xl font-bold">Learners</h1>
<p className="text-muted-foreground">Manage and track all learners in your organization</p>
</div>
<div className="flex items-center gap-2">
<Button
variant="outline"
size="sm"
onClick={() => setViewMode(viewMode === 'table' ? 'grid' : 'table')}
className="min-tap-44"
>
{viewMode === 'table' ? 'Grid View' : 'Table View'}
</Button>
</div>
</div>
{/* Stats Cards */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<Card>
<CardContent className="pt-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-muted-foreground">Total Learners</p>
<p className="text-2xl font-bold">{employees.length}</p>
</div>
<Users className="h-8 w-8 text-blue-500" />
</div>
</CardContent>
</Card>
<Card>
<CardContent className="pt-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-muted-foreground">Active Now</p>
<p className="text-2xl font-bold">{employees.filter(e => e.status === 'Active').length}</p>
</div>
<CheckCircle className="h-8 w-8 text-green-500" />
</div>
</CardContent>
</Card>
<Card>
<CardContent className="pt-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-muted-foreground">Pending</p>
<p className="text-2xl font-bold">{employees.filter(e => e.status === 'Pending').length}</p>
</div>
<Clock className="h-8 w-8 text-yellow-500" />
</div>
</CardContent>
</Card>
<Card>
<CardContent className="pt-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-muted-foreground">Avg. Progress</p>
<p className="text-2xl font-bold">
{Math.round(employees.reduce((acc, e) => acc + (e.progress || 0), 0) / employees.length)}%
</p>
</div>
<Award className="h-8 w-8 text-purple-500" />
</div>
</CardContent>
</Card>
</div>
{/* Toolbar */}
<Card>
<CardContent className="pt-6">
<div className="flex flex-col lg:flex-row gap-4 items-start lg:items-center justify-between">
<div className="flex flex-col sm:flex-row gap-4 items-start sm:items-center flex-1">
<div className="relative flex-1 max-w-md">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
placeholder="Search learners..."
className="pl-10"
onChange={(e) => debouncedSearch(e.target.value)}
aria-label="Search learners by name or email"
/>
</div>
<Select value={statusFilter} onValueChange={setStatusFilter}>
<SelectTrigger className="w-[150px]">
<Filter className="h-4 w-4 mr-2" />
<SelectValue placeholder="Status" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Status</SelectItem>
<SelectItem value="Active">Active</SelectItem>
<SelectItem value="Inactive">Inactive</SelectItem>
<SelectItem value="Pending">Pending</SelectItem>
</SelectContent>
</Select>
<Select value={programmeFilter} onValueChange={setProgrammeFilter}>
<SelectTrigger className="w-[200px]">
<BookOpen className="h-4 w-4 mr-2" />
<SelectValue placeholder="Programme" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Programmes</SelectItem>
{programmes.map(prog => (
<SelectItem key={prog} value={prog}>{prog}</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="flex gap-2">
<Button
onClick={() => setShowAddDrawer(true)}
className="min-tap-44"
>
<Plus className="h-4 w-4 mr-2" />
Add Learner
</Button>
<Button
variant="outline"
onClick={() => setShowImportModal(true)}
className="min-tap-44"
>
<Upload className="h-4 w-4 mr-2" />
Import
</Button>
<Button
variant="outline"
onClick={() => handleExport('excel')}
disabled={loading}
className="min-tap-44"
>
{loading ? (
<RefreshCw className="h-4 w-4 mr-2 animate-spin" />
) : (
<Download className="h-4 w-4 mr-2" />
)}
Export
</Button>
</div>
</div>
</CardContent>
</Card>
{/* Bulk Action Bar */}
{bulkActionVisible && (
<Card className="animate-slide-in-right">
<CardContent className="py-4">
<div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground" aria-live="polite">
{selectedEmployees.length} learner{selectedEmployees.length !== 1 ? 's' : ''} selected
</span>
<div className="flex gap-2">
<Button
variant="outline"
size="sm"
onClick={() => setShowAssignModal(true)}
className="min-tap-44"
>
<UserPlus className="h-4 w-4 mr-2" />
Assign to Programme
</Button>
<Button
variant="outline"
size="sm"
className="min-tap-44"
>
<Mail className="h-4 w-4 mr-2" />
Send Email
</Button>
<Button
variant="outline"
size="sm"
className="min-tap-44 text-red-600"
>
<XCircle className="h-4 w-4 mr-2" />
Deactivate
</Button>
</div>
</div>
</CardContent>
</Card>
)}
{/* Learners Table/Grid */}
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<div>
<CardTitle>Learners ({filteredEmployees.length})</CardTitle>
<CardDescription>
{viewMode === 'table' ? 'Manage learner accounts and assignments' : 'Grid view of all learners'}
Showing {startIndex + 1}-{Math.min(endIndex, filteredEmployees.length)} of {filteredEmployees.length} learners
</CardDescription>
</div>
<div className="flex items-center gap-2">
<Select value={itemsPerPage.toString()} onValueChange={(value) => {
setItemsPerPage(Number(value));
setCurrentPage(1);
setSelectedEmployees([]);
}}>
<SelectTrigger className="w-[100px]">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="5">5</SelectItem>
<SelectItem value="10">10</SelectItem>
<SelectItem value="20">20</SelectItem>
<SelectItem value="50">50</SelectItem>
</SelectContent>
</Select>
<Button
variant="outline"
size="sm"
onClick={() => handleBulkSelect(selectedEmployees.length !== paginatedEmployees.length)}
className="min-tap-44"
>
{selectedEmployees.length === paginatedEmployees.length && paginatedEmployees.length > 0 ? 'Deselect All' : 'Select All'}
</Button>
</div>
</div>
</CardHeader>
<CardContent>
{viewMode === 'table' ? (
<div className="rounded-md border min-h-[60vh]">
<Table>
<TableHeader className="sticky-header">
<TableRow>
<TableHead className="w-[50px]">Select</TableHead>
<TableHead className="w-[200px]">Name</TableHead>
<TableHead className="w-[250px]">Email</TableHead>
<TableHead className="w-[150px]">Phone</TableHead>
<TableHead className="w-[150px]">Programme</TableHead>
<TableHead className="w-[100px]">Progress</TableHead>
<TableHead className="w-[100px]">Status</TableHead>
<TableHead className="w-[120px]">Last Activity</TableHead>
<TableHead className="w-[80px]">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{paginatedEmployees.map((employee) => (
<TableRow key={employee.id} className="min-h-[48px]">
<TableCell>
<Checkbox
checked={selectedEmployees.includes(employee.id)}
onCheckedChange={(checked) => handleEmployeeSelect(employee.id, !!checked)}
className="min-tap-44"
aria-label={`Select ${employee.name}`}
/>
</TableCell>
<TableCell className="font-medium">{employee.name}</TableCell>
<TableCell className="text-muted-foreground">{employee.email}</TableCell>
<TableCell className="text-muted-foreground">{employee.phone}</TableCell>
<TableCell>{employee.programme || '-'}</TableCell>
<TableCell>
<div className="flex items-center gap-2">
<Progress value={employee.progress} className="w-16" />
<span className="text-sm">{employee.progress}%</span>
</div>
</TableCell>
<TableCell>
<Badge variant={getStatusColor(employee.status)}>
{employee.status}
</Badge>
</TableCell>
<TableCell className="text-sm text-muted-foreground">
{employee.lastActivity}
</TableCell>
<TableCell>
<div className="flex gap-1">
<Button
variant="ghost"
size="sm"
onClick={() => handleEditEmployee(employee)}
className="min-tap-44"
aria-label={`Edit ${employee.name}`}
>
<Edit className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="sm"
className="min-tap-44"
aria-label={`More actions for ${employee.name}`}
>
<MoreHorizontal className="h-4 w-4" />
</Button>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 min-h-[60vh]">
{paginatedEmployees.map((employee) => (
<Card key={employee.id} className="cursor-pointer hover:shadow-lg transition-all">
<CardContent className="pt-6">
<div className="flex items-start justify-between mb-4">
<div className="flex items-center gap-3">
<div className="w-12 h-12 bg-primary/10 rounded-full flex items-center justify-center">
<span className="text-lg font-semibold text-primary">
{employee.name.split(' ').map(n => n[0]).join('')}
</span>
</div>
<div>
<p className="font-semibold">{employee.name}</p>
<p className="text-sm text-muted-foreground">{employee.email}</p>
</div>
</div>
<Checkbox
checked={selectedEmployees.includes(employee.id)}
onCheckedChange={(checked) => handleEmployeeSelect(employee.id, !!checked)}
className="min-tap-44"
/>
</div>
<div className="space-y-3">
<div className="flex items-center gap-2 text-sm">
<Phone className="h-4 w-4 text-muted-foreground" />
<span>{employee.phone}</span>
</div>
<div className="flex items-center gap-2 text-sm">
<BookOpen className="h-4 w-4 text-muted-foreground" />
<span>{employee.programme || 'No programme assigned'}</span>
</div>
<div className="space-y-1">
<div className="flex justify-between text-sm">
<span>Progress</span>
<span className="font-medium">{employee.progress}%</span>
</div>
<Progress value={employee.progress} className="h-2" />
</div>
<div className="flex items-center justify-between pt-2">
<Badge variant={getStatusColor(employee.status)}>
{employee.status}
</Badge>
<span className="text-xs text-muted-foreground">
{employee.lastActivity}
</span>
</div>
</div>
</CardContent>
</Card>
))}
</div>
)}
{/* Pagination */}
{totalPages > 1 && (
<div className="mt-4 flex items-center justify-between">
<p className="text-sm text-muted-foreground">
Page {currentPage} of {totalPages}
</p>
<div className="flex items-center gap-2">
<Button
variant="outline"
size="sm"
onClick={() => handlePageChange(currentPage - 1)}
disabled={currentPage === 1}
className="min-tap-44"
>
<ChevronLeft className="h-4 w-4" />
</Button>
{Array.from({ length: Math.min(5, totalPages) }, (_, i) => {
let pageNum;
if (totalPages <= 5) {
pageNum = i + 1;
} else if (currentPage <= 3) {
pageNum = i + 1;
} else if (currentPage >= totalPages - 2) {
pageNum = totalPages - 4 + i;
} else {
pageNum = currentPage - 2 + i;
}
return (
<Button
key={pageNum}
variant={currentPage === pageNum ? 'default' : 'outline'}
size="sm"
onClick={() => handlePageChange(pageNum)}
className="min-tap-44"
>
{pageNum}
</Button>
);
})}
<Button
variant="outline"
size="sm"
onClick={() => handlePageChange(currentPage + 1)}
disabled={currentPage === totalPages}
className="min-tap-44"
>
<ChevronRight className="h-4 w-4" />
</Button>
</div>
</div>
)}
</CardContent>
</Card>
{/* Add Learner Drawer */}
<Sheet open={showAddDrawer} onOpenChange={setShowAddDrawer}>
<SheetContent className="w-[600px] sm:w-[700px]">
<SheetHeader>
<SheetTitle>Add New Learner</SheetTitle>
<SheetDescription>
Add a new learner to the system. Email cannot be changed after saving.
</SheetDescription>
</SheetHeader>
<div className="mt-6 space-y-6">
<div>
<label className="block text-sm font-medium mb-2">
Full Name *
</label>
<Input
value={newEmployee.name}
onChange={(e) => setNewEmployee(prev => ({ ...prev, name: e.target.value }))}
placeholder="Enter full name"
required
/>
</div>
<div>
<label className="block text-sm font-medium mb-2">
Email Address *
</label>
<Input
type="email"
value={newEmployee.email}
onChange={(e) => setNewEmployee(prev => ({ ...prev, email: e.target.value }))}
placeholder="email@company.com"
required
/>
</div>
<div>
<label className="block text-sm font-medium mb-2">
Phone Number
</label>
<Input
type="tel"
value={newEmployee.phone}
onChange={(e) => setNewEmployee(prev => ({ ...prev, phone: e.target.value }))}
placeholder="+61 4XX XXX XXX"
/>
</div>
<div className="flex gap-2 pt-4">
<Button onClick={handleAddEmployee} className="flex-1">
Save Learner
</Button>
<Button variant="outline" onClick={() => setShowAddDrawer(false)} className="flex-1">
Cancel
</Button>
</div>
</div>
</SheetContent>
</Sheet>
{/* Import Modal */}
<Dialog open={showImportModal} onOpenChange={setShowImportModal}>
<DialogContent className="sm:max-w-[600px]">
<DialogHeader>
<DialogTitle>Import Learners</DialogTitle>
<DialogDescription>
Upload a CSV file to import multiple learners. Maximum file size: 5MB.
</DialogDescription>
</DialogHeader>
<div className="space-y-6">
<div className="space-y-2">
<h4 className="font-medium">Step 1: Download Template</h4>
<p className="text-sm text-muted-foreground">
Download our CSV template with the required fields: Name, Email, Phone.
</p>
<Button variant="outline" className="min-tap-44">
<Download className="h-4 w-4 mr-2" />
Download CSV Template
</Button>
</div>
<div className="space-y-2">
<h4 className="font-medium">Step 2: Upload File</h4>
<div className="border-2 border-dashed border-muted rounded-lg p-8 text-center">
<Upload className="h-8 w-8 mx-auto mb-2 text-muted-foreground" />
<p className="text-sm text-muted-foreground">
Drag and drop your CSV file here, or click to browse
</p>
<Button variant="outline" className="mt-2 min-tap-44">
Choose File
</Button>
</div>
</div>
<div className="flex gap-2">
<Button className="flex-1">Import Learners</Button>
<Button variant="outline" onClick={() => setShowImportModal(false)} className="flex-1">
Cancel
</Button>
</div>
</div>
</DialogContent>
</Dialog>
{/* Assign Modal */}
<Dialog open={showAssignModal} onOpenChange={setShowAssignModal}>
<DialogContent className="sm:max-w-[500px]">
<DialogHeader>
<DialogTitle>Assign to Programme</DialogTitle>
<DialogDescription>
Assign {selectedEmployees.length} selected learner{selectedEmployees.length !== 1 ? 's' : ''} to a programme.
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium mb-2">
Select Programme
</label>
<Select>
<SelectTrigger>
<SelectValue placeholder="Choose programme" />
</SelectTrigger>
<SelectContent>
<SelectItem value="leadership">Leadership Development</SelectItem>
<SelectItem value="technical">Technical Skills</SelectItem>
<SelectItem value="communication">Communication</SelectItem>
<SelectItem value="project">Project Management</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<label className="block text-sm font-medium mb-2">
Start Date
</label>
<Input type="date" />
</div>
<div>
<label className="block text-sm font-medium mb-2">
End Date
</label>
<Input type="date" />
</div>
<div className="flex gap-2 pt-4">
<Button className="flex-1">Assign</Button>
<Button variant="outline" onClick={() => setShowAssignModal(false)} className="flex-1">
Cancel
</Button>
</div>
</div>
</DialogContent>
</Dialog>
{/* Edit Drawer */}
<Sheet open={showEditDrawer} onOpenChange={setShowEditDrawer}>
<SheetContent className="w-[600px] sm:w-[700px]">
<SheetHeader>
<SheetTitle>Edit Learner</SheetTitle>
<SheetDescription>
Update learner information and manage assignments.
</SheetDescription>
</SheetHeader>
{editingEmployee && (
<Tabs defaultValue="details" className="mt-6">
<TabsList className="grid w-full grid-cols-3">
<TabsTrigger value="details">Details</TabsTrigger>
<TabsTrigger value="progress">Progress</TabsTrigger>
<TabsTrigger value="assignments">Assignments</TabsTrigger>
</TabsList>
<TabsContent value="details" className="space-y-4 mt-4">
<div>
<label className="block text-sm font-medium mb-2">Name</label>
<Input defaultValue={editingEmployee.name} />
</div>
<div>
<label className="block text-sm font-medium mb-2">Email</label>
<Input defaultValue={editingEmployee.email} readOnly className="bg-muted" />
</div>
<div>
<label className="block text-sm font-medium mb-2">Phone</label>
<Input defaultValue={editingEmployee.phone} />
</div>
<div>
<label className="block text-sm font-medium mb-2">Status</label>
<Select defaultValue={editingEmployee.status}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="Active">Active</SelectItem>
<SelectItem value="Inactive">Inactive</SelectItem>
<SelectItem value="Pending">Pending</SelectItem>
</SelectContent>
</Select>
</div>
</TabsContent>
<TabsContent value="progress" className="space-y-4 mt-4">
<div>
<label className="block text-sm font-medium mb-2">Current Progress</label>
<div className="flex items-center gap-4">
<Progress value={editingEmployee.progress} className="flex-1" />
<span className="text-lg font-semibold">{editingEmployee.progress}%</span>
</div>
</div>
<div>
<label className="block text-sm font-medium mb-2">Last Activity</label>
<p className="text-muted-foreground">{editingEmployee.lastActivity}</p>
</div>
</TabsContent>
<TabsContent value="assignments" className="space-y-4 mt-4">
{editingEmployee.programme ? (
<Card>
<CardContent className="pt-4">
<div className="flex items-center justify-between">
<div>
<p className="font-medium">{editingEmployee.programme}</p>
<p className="text-sm text-muted-foreground">{editingEmployee.course}</p>
</div>
<Button variant="outline" size="sm">Change</Button>
</div>
</CardContent>
</Card>
) : (
<div className="text-center py-8 text-muted-foreground">
<BookOpen className="h-12 w-12 mx-auto mb-4" />
<p>No programmes assigned</p>
<Button className="mt-4">Assign Programme</Button>
</div>
)}
</TabsContent>
<div className="flex gap-2 mt-6">
<Button className="flex-1">Save Changes</Button>
<Button variant="outline" onClick={() => setShowEditDrawer(false)} className="flex-1">
Cancel
</Button>
</div>
</Tabs>
)}
</SheetContent>
</Sheet>
</div>
);
};
export default LearnersPage;

View File

@@ -0,0 +1,519 @@
import React, { useState, useEffect } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { Button } from '../../components/ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../../components/ui/card';
import { Badge } from '../../components/ui/badge';
import { Progress } from '../../components/ui/progress';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '../../components/ui/tabs';
import { Avatar, AvatarFallback } from '../../components/ui/avatar';
import {
ArrowLeft,
Users,
BookOpen,
Clock,
Calendar,
Award,
Download,
UserPlus,
BarChart3,
MessageSquare,
FileText,
TrendingUp,
CheckCircle,
AlertCircle,
PlayCircle,
FileCheck,
Video,
File,
Link as LinkIcon,
MoreHorizontal,
Edit,
Trash2,
Share2
} from 'lucide-react';
interface Programme {
id: string;
name: string;
description: string;
duration: string;
startDate: string;
endDate: string;
status: 'Active' | 'Upcoming' | 'Completed';
enrolledCount: number;
capacity: number;
completionRate: number;
averageScore: number;
instructor: {
name: string;
email: string;
avatar?: string;
};
modules: Module[];
resources: Resource[];
}
interface Module {
id: string;
title: string;
description: string;
duration: string;
type: 'video' | 'reading' | 'quiz' | 'assignment';
status: 'locked' | 'available' | 'completed';
progress?: number;
}
interface Resource {
id: string;
title: string;
type: 'pdf' | 'video' | 'link' | 'document';
url: string;
size?: string;
}
interface EnrolledLearner {
id: string;
name: string;
email: string;
progress: number;
status: 'Active' | 'At Risk' | 'Completed';
lastActivity: string;
}
// Mock data
const mockProgramme: Programme = {
id: '1',
name: 'Leadership Development Program',
description: 'Comprehensive leadership program designed for emerging leaders to develop essential management skills, strategic thinking, and team leadership capabilities.',
duration: '12 weeks',
startDate: '2024-01-15',
endDate: '2024-04-05',
status: 'Active',
enrolledCount: 45,
capacity: 50,
completionRate: 78,
averageScore: 85,
instructor: {
name: 'Dr. Sarah Johnson',
email: 'sarah.johnson@klc.edu'
},
modules: [
{
id: 'm1',
title: 'Foundations of Leadership',
description: 'Understanding leadership styles and core principles',
duration: '2 weeks',
type: 'video',
status: 'completed',
progress: 100
},
{
id: 'm2',
title: 'Strategic Thinking',
description: 'Developing strategic mindset and decision-making',
duration: '3 weeks',
type: 'reading',
status: 'completed',
progress: 100
},
{
id: 'm3',
title: 'Team Building & Management',
description: 'Building and leading high-performance teams',
duration: '3 weeks',
type: 'assignment',
status: 'available',
progress: 65
},
{
id: 'm4',
title: 'Communication & Influence',
description: 'Effective communication and influencing skills',
duration: '2 weeks',
type: 'video',
status: 'available',
progress: 30
},
{
id: 'm5',
title: 'Change Management',
description: 'Leading through organizational change',
duration: '2 weeks',
type: 'quiz',
status: 'locked',
progress: 0
}
],
resources: [
{ id: 'r1', title: 'Leadership Assessment Tool', type: 'pdf', url: '#', size: '2.5 MB' },
{ id: 'r2', title: 'Strategic Planning Template', type: 'document', url: '#', size: '1.8 MB' },
{ id: 'r3', title: 'Team Building Activities Guide', type: 'pdf', url: '#', size: '3.2 MB' },
{ id: 'r4', title: 'Communication Framework Video', type: 'video', url: '#', size: '45 MB' }
]
};
const mockEnrolledLearners: EnrolledLearner[] = [
{ id: '1', name: 'Sarah Chen', email: 'sarah.chen@company.com', progress: 92, status: 'Active', lastActivity: '2 hours ago' },
{ id: '2', name: 'Michael Rodriguez', email: 'michael.r@company.com', progress: 78, status: 'Active', lastActivity: '1 day ago' },
{ id: '3', name: 'Emma Thompson', email: 'emma.thompson@company.com', progress: 45, status: 'At Risk', lastActivity: '5 days ago' },
{ id: '4', name: 'David Kim', email: 'david.kim@company.com', progress: 88, status: 'Active', lastActivity: '3 hours ago' },
{ id: '5', name: 'Lisa Wang', email: 'lisa.wang@company.com', progress: 95, status: 'Completed', lastActivity: '1 day ago' }
];
const ProgrammeViewPage: React.FC = () => {
const { id } = useParams<{ id: string }>();
const navigate = useNavigate();
const [programme, setProgramme] = useState<Programme | null>(null);
const [loading, setLoading] = useState(true);
const [activeTab, setActiveTab] = useState('overview');
useEffect(() => {
// Simulate API call
const timer = setTimeout(() => {
setProgramme(mockProgramme);
setLoading(false);
}, 500);
return () => clearTimeout(timer);
}, [id]);
if (loading) {
return (
<div className="space-y-6">
<div className="h-8 w-64 bg-muted animate-pulse rounded" />
<div className="h-32 bg-muted animate-pulse rounded" />
<div className="h-64 bg-muted animate-pulse rounded" />
</div>
);
}
if (!programme) {
return (
<div className="text-center py-12">
<AlertCircle className="h-12 w-12 mx-auto mb-4 text-muted-foreground" />
<h2 className="text-xl font-semibold mb-2">Programme not found</h2>
<p className="text-muted-foreground mb-4">The programme you're looking for doesn't exist.</p>
<Button onClick={() => navigate('/dashboard')}>Back to Dashboard</Button>
</div>
);
}
const getStatusColor = (status: string) => {
switch (status) {
case 'Active': return 'default';
case 'Completed': return 'secondary';
case 'locked': return 'outline';
case 'available': return 'default';
default: return 'secondary';
}
};
const getModuleIcon = (type: string) => {
switch (type) {
case 'video': return <Video className="h-4 w-4" />;
case 'reading': return <File className="h-4 w-4" />;
case 'quiz': return <FileCheck className="h-4 w-4" />;
case 'assignment': return <FileText className="h-4 w-4" />;
default: return <BookOpen className="h-4 w-4" />;
}
};
return (
<div className="space-y-6">
{/* Header */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<Button
variant="ghost"
size="sm"
onClick={() => navigate(-1)}
className="min-tap-44"
>
<ArrowLeft className="h-4 w-4 mr-2" />
Back
</Button>
<div>
<h1 className="text-2xl font-bold">{programme.name}</h1>
<p className="text-muted-foreground">Programme Overview</p>
</div>
</div>
<div className="flex items-center gap-2">
<Button variant="outline" size="sm" className="min-tap-44">
<Edit className="h-4 w-4 mr-2" />
Edit
</Button>
<Button variant="outline" size="sm" className="min-tap-44">
<Share2 className="h-4 w-4 mr-2" />
Share
</Button>
<Button variant="outline" size="sm" className="min-tap-44 text-red-600">
<Trash2 className="h-4 w-4 mr-2" />
Delete
</Button>
</div>
</div>
{/* Programme Stats */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<Card>
<CardContent className="pt-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-muted-foreground">Enrolled</p>
<p className="text-2xl font-bold">
{programme.enrolledCount}/{programme.capacity}
</p>
</div>
<Users className="h-8 w-8 text-blue-500" />
</div>
</CardContent>
</Card>
<Card>
<CardContent className="pt-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-muted-foreground">Completion Rate</p>
<p className="text-2xl font-bold">{programme.completionRate}%</p>
</div>
<TrendingUp className="h-8 w-8 text-green-500" />
</div>
</CardContent>
</Card>
<Card>
<CardContent className="pt-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-muted-foreground">Average Score</p>
<p className="text-2xl font-bold">{programme.averageScore}/100</p>
</div>
<Award className="h-8 w-8 text-yellow-500" />
</div>
</CardContent>
</Card>
<Card>
<CardContent className="pt-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-muted-foreground">Duration</p>
<p className="text-2xl font-bold">{programme.duration}</p>
</div>
<Clock className="h-8 w-8 text-purple-500" />
</div>
</CardContent>
</Card>
</div>
{/* Main Content Tabs */}
<Card>
<CardContent className="pt-6">
<Tabs value={activeTab} onValueChange={setActiveTab}>
<TabsList className="grid w-full grid-cols-4">
<TabsTrigger value="overview">Overview</TabsTrigger>
<TabsTrigger value="modules">Modules</TabsTrigger>
<TabsTrigger value="learners">Learners</TabsTrigger>
<TabsTrigger value="resources">Resources</TabsTrigger>
</TabsList>
{/* Overview Tab */}
<TabsContent value="overview" className="space-y-6 mt-6">
<div>
<h3 className="font-semibold mb-2">Description</h3>
<p className="text-muted-foreground">{programme.description}</p>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<h4 className="text-sm font-medium mb-2">Programme Details</h4>
<div className="space-y-2">
<div className="flex justify-between text-sm">
<span className="text-muted-foreground">Start Date:</span>
<span>{new Date(programme.startDate).toLocaleDateString()}</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-muted-foreground">End Date:</span>
<span>{new Date(programme.endDate).toLocaleDateString()}</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-muted-foreground">Status:</span>
<Badge variant={getStatusColor(programme.status)}>
{programme.status}
</Badge>
</div>
</div>
</div>
<div>
<h4 className="text-sm font-medium mb-2">Instructor</h4>
<div className="flex items-center gap-3">
<Avatar>
<AvatarFallback>
{programme.instructor.name.split(' ').map(n => n[0]).join('')}
</AvatarFallback>
</Avatar>
<div>
<p className="font-medium">{programme.instructor.name}</p>
<p className="text-sm text-muted-foreground">{programme.instructor.email}</p>
</div>
</div>
</div>
</div>
<div>
<h3 className="font-semibold mb-4">Quick Actions</h3>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<Button variant="outline" className="h-auto py-4 flex-col gap-2">
<UserPlus className="h-6 w-6" />
<span>Add Learners</span>
</Button>
<Button variant="outline" className="h-auto py-4 flex-col gap-2">
<Download className="h-6 w-6" />
<span>Export Data</span>
</Button>
<Button variant="outline" className="h-auto py-4 flex-col gap-2">
<BarChart3 className="h-6 w-6" />
<span>Analytics</span>
</Button>
<Button variant="outline" className="h-auto py-4 flex-col gap-2">
<MessageSquare className="h-6 w-6" />
<span>Discussions</span>
</Button>
</div>
</div>
</TabsContent>
{/* Modules Tab */}
<TabsContent value="modules" className="space-y-4 mt-6">
{programme.modules.map((module, index) => (
<Card key={module.id}>
<CardContent className="pt-6">
<div className="flex items-start gap-4">
<div className="w-8 h-8 bg-primary/10 rounded-full flex items-center justify-center">
{getModuleIcon(module.type)}
</div>
<div className="flex-1">
<div className="flex items-start justify-between">
<div>
<h4 className="font-semibold">{module.title}</h4>
<p className="text-sm text-muted-foreground">
{module.description}
</p>
</div>
<Badge variant={getStatusColor(module.status)}>
{module.status}
</Badge>
</div>
<div className="flex items-center gap-4 mt-2 text-sm text-muted-foreground">
<span className="flex items-center gap-1">
<Clock className="h-4 w-4" />
{module.duration}
</span>
<span className="flex items-center gap-1">
{getModuleIcon(module.type)}
{module.type.charAt(0).toUpperCase() + module.type.slice(1)}
</span>
</div>
{module.progress > 0 && (
<div className="mt-3">
<div className="flex justify-between text-sm mb-1">
<span>Progress</span>
<span>{module.progress}%</span>
</div>
<Progress value={module.progress} className="h-2" />
</div>
)}
</div>
</div>
</CardContent>
</Card>
))}
</TabsContent>
{/* Learners Tab */}
<TabsContent value="learners" className="space-y-4 mt-6">
<div className="flex justify-between items-center">
<h3 className="font-semibold">Enrolled Learners ({programme.enrolledCount})</h3>
<Button size="sm">
<UserPlus className="h-4 w-4 mr-2" />
Add Learners
</Button>
</div>
<div className="space-y-3">
{mockEnrolledLearners.map(learner => (
<Card key={learner.id}>
<CardContent className="pt-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<Avatar>
<AvatarFallback>
{learner.name.split(' ').map(n => n[0]).join('')}
</AvatarFallback>
</Avatar>
<div>
<p className="font-medium">{learner.name}</p>
<p className="text-sm text-muted-foreground">{learner.email}</p>
</div>
</div>
<div className="flex items-center gap-4">
<div className="text-right">
<p className="text-sm font-medium">{learner.progress}%</p>
<Progress value={learner.progress} className="w-24" />
</div>
<Badge variant={
learner.status === 'Active' ? 'default' :
learner.status === 'At Risk' ? 'destructive' : 'secondary'
}>
{learner.status}
</Badge>
<Button variant="ghost" size="sm">
<MoreHorizontal className="h-4 w-4" />
</Button>
</div>
</div>
</CardContent>
</Card>
))}
</div>
</TabsContent>
{/* Resources Tab */}
<TabsContent value="resources" className="space-y-4 mt-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{programme.resources.map(resource => (
<Card key={resource.id}>
<CardContent className="pt-4">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-primary/10 rounded-lg flex items-center justify-center">
{resource.type === 'pdf' && <FileText className="h-5 w-5" />}
{resource.type === 'video' && <Video className="h-5 w-5" />}
{resource.type === 'link' && <LinkIcon className="h-5 w-5" />}
{resource.type === 'document' && <File className="h-5 w-5" />}
</div>
<div className="flex-1">
<h4 className="font-medium">{resource.title}</h4>
<p className="text-xs text-muted-foreground">
{resource.type.toUpperCase()} {resource.size}
</p>
</div>
<Button variant="ghost" size="sm">
<Download className="h-4 w-4" />
</Button>
</div>
</CardContent>
</Card>
))}
</div>
</TabsContent>
</Tabs>
</CardContent>
</Card>
</div>
);
};
export default ProgrammeViewPage;

View File

@@ -0,0 +1,574 @@
import React, { useState } from 'react';
import { Button } from '../../components/ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../../components/ui/card';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '../../components/ui/tabs';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../../components/ui/select';
import { Badge } from '../../components/ui/badge';
import { Progress } from '../../components/ui/progress';
import {
BarChart3,
Download,
FileText,
TrendingUp,
Users,
BookOpen,
Clock,
Award,
Calendar,
Filter,
RefreshCw,
ChevronDown,
PieChart,
LineChart,
Table as TableIcon,
Eye,
Mail,
AlertCircle,
CheckCircle,
XCircle,
DownloadCloud
} from 'lucide-react';
import {
LineChart as ReLineChart,
Line,
AreaChart,
Area,
BarChart,
Bar,
PieChart as RePieChart,
Pie,
Cell,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
Legend,
ResponsiveContainer
} from 'recharts';
// Mock data for charts
const completionTrendData = [
{ month: 'Jan', completion: 65, enrollment: 78 },
{ month: 'Feb', completion: 68, enrollment: 82 },
{ month: 'Mar', completion: 72, enrollment: 85 },
{ month: 'Apr', completion: 70, enrollment: 88 },
{ month: 'May', completion: 75, enrollment: 92 },
{ month: 'Jun', completion: 78, enrollment: 95 },
{ month: 'Jul', completion: 80, enrollment: 98 },
{ month: 'Aug', completion: 82, enrollment: 100 },
{ month: 'Sep', completion: 85, enrollment: 102 },
{ month: 'Oct', completion: 83, enrollment: 105 },
{ month: 'Nov', completion: 87, enrollment: 108 },
{ month: 'Dec', completion: 90, enrollment: 110 }
];
const programmePerformanceData = [
{ name: 'Leadership', completion: 85, enrollment: 120 },
{ name: 'Technical', completion: 72, enrollment: 95 },
{ name: 'Communication', completion: 88, enrollment: 80 },
{ name: 'Project Mgmt', completion: 78, enrollment: 110 },
{ name: 'Sales', completion: 82, enrollment: 70 }
];
const learnerStatusData = [
{ name: 'Active', value: 450, color: '#22c55e' },
{ name: 'At Risk', value: 85, color: '#ef4444' },
{ name: 'Completed', value: 320, color: '#3b82f6' },
{ name: 'Pending', value: 45, color: '#eab308' }
];
const activityData = [
{ day: 'Mon', video: 45, reading: 30, quiz: 20 },
{ day: 'Tue', video: 52, reading: 35, quiz: 25 },
{ day: 'Wed', video: 48, reading: 42, quiz: 28 },
{ day: 'Thu', video: 55, reading: 38, quiz: 32 },
{ day: 'Fri', video: 50, reading: 40, quiz: 30 },
{ day: 'Sat', video: 35, reading: 25, quiz: 15 },
{ day: 'Sun', video: 30, reading: 20, quiz: 12 }
];
const ReportsPage: React.FC = () => {
const [dateRange, setDateRange] = useState('last-30-days');
const [selectedProgramme, setSelectedProgramme] = useState('all');
const [exporting, setExporting] = useState(false);
const [activeTab, setActiveTab] = useState('overview');
const programmes = [
{ id: 'all', name: 'All Programmes' },
{ id: 'leadership', name: 'Leadership Development' },
{ id: 'technical', name: 'Technical Skills' },
{ id: 'communication', name: 'Communication' },
{ id: 'project', name: 'Project Management' }
];
const handleExport = async (format: 'excel' | 'pdf' | 'csv') => {
setExporting(true);
await new Promise(resolve => setTimeout(resolve, 2000));
setExporting(false);
console.log(`Exported report as ${format}`);
};
return (
<div className="space-y-6">
{/* Header */}
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div>
<h1 className="text-2xl font-bold">Reports & Analytics</h1>
<p className="text-muted-foreground">Track performance, engagement, and completion metrics</p>
</div>
<div className="flex items-center gap-2">
<Select value={selectedProgramme} onValueChange={setSelectedProgramme}>
<SelectTrigger className="w-[200px]">
<SelectValue placeholder="Select Programme" />
</SelectTrigger>
<SelectContent>
{programmes.map(p => (
<SelectItem key={p.id} value={p.id}>{p.name}</SelectItem>
))}
</SelectContent>
</Select>
<Select value={dateRange} onValueChange={setDateRange}>
<SelectTrigger className="w-[150px]">
<SelectValue placeholder="Date Range" />
</SelectTrigger>
<SelectContent>
<SelectItem value="last-7-days">Last 7 days</SelectItem>
<SelectItem value="last-30-days">Last 30 days</SelectItem>
<SelectItem value="last-90-days">Last 90 days</SelectItem>
<SelectItem value="this-year">This Year</SelectItem>
</SelectContent>
</Select>
<Button
variant="outline"
onClick={() => handleExport('excel')}
disabled={exporting}
className="min-tap-44"
>
{exporting ? (
<RefreshCw className="h-4 w-4 mr-2 animate-spin" />
) : (
<DownloadCloud className="h-4 w-4 mr-2" />
)}
Export
</Button>
</div>
</div>
{/* Overview Stats */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<Card>
<CardContent className="pt-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-muted-foreground">Total Learners</p>
<p className="text-2xl font-bold">1,247</p>
<p className="text-xs text-green-600">+12% from last month</p>
</div>
<Users className="h-8 w-8 text-blue-500" />
</div>
</CardContent>
</Card>
<Card>
<CardContent className="pt-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-muted-foreground">Completion Rate</p>
<p className="text-2xl font-bold">78%</p>
<p className="text-xs text-green-600">+5% from last month</p>
</div>
<CheckCircle className="h-8 w-8 text-green-500" />
</div>
</CardContent>
</Card>
<Card>
<CardContent className="pt-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-muted-foreground">Avg. Time to Complete</p>
<p className="text-2xl font-bold">21 days</p>
<p className="text-xs text-red-600">+2 days from last month</p>
</div>
<Clock className="h-8 w-8 text-yellow-500" />
</div>
</CardContent>
</Card>
<Card>
<CardContent className="pt-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-muted-foreground">Active Programmes</p>
<p className="text-2xl font-bold">12</p>
<p className="text-xs text-green-600">+3 from last month</p>
</div>
<BookOpen className="h-8 w-8 text-purple-500" />
</div>
</CardContent>
</Card>
</div>
{/* Main Tabs */}
<Card>
<CardContent className="pt-6">
<Tabs value={activeTab} onValueChange={setActiveTab}>
<TabsList className="grid w-full grid-cols-4">
<TabsTrigger value="overview">Overview</TabsTrigger>
<TabsTrigger value="programmes">Programmes</TabsTrigger>
<TabsTrigger value="learners">Learners</TabsTrigger>
<TabsTrigger value="engagement">Engagement</TabsTrigger>
</TabsList>
{/* Overview Tab */}
<TabsContent value="overview" className="space-y-6 mt-6">
{/* Completion Trends */}
<Card>
<CardHeader>
<CardTitle>Completion Trends</CardTitle>
<CardDescription>Monthly completion and enrollment rates</CardDescription>
</CardHeader>
<CardContent>
<div className="h-80">
<ResponsiveContainer width="100%" height="100%">
<AreaChart data={completionTrendData}>
<defs>
<linearGradient id="completionGradient" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#3b82f6" stopOpacity={0.3}/>
<stop offset="95%" stopColor="#3b82f6" stopOpacity={0}/>
</linearGradient>
<linearGradient id="enrollmentGradient" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#22c55e" stopOpacity={0.3}/>
<stop offset="95%" stopColor="#22c55e" stopOpacity={0}/>
</linearGradient>
</defs>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="month" />
<YAxis />
<Tooltip />
<Legend />
<Area
type="monotone"
dataKey="completion"
stroke="#3b82f6"
fillOpacity={1}
fill="url(#completionGradient)"
name="Completion Rate (%)"
/>
<Area
type="monotone"
dataKey="enrollment"
stroke="#22c55e"
fillOpacity={1}
fill="url(#enrollmentGradient)"
name="Enrollment Count"
/>
</AreaChart>
</ResponsiveContainer>
</div>
</CardContent>
</Card>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Programme Performance */}
<Card>
<CardHeader>
<CardTitle>Programme Performance</CardTitle>
<CardDescription>Completion rates by programme</CardDescription>
</CardHeader>
<CardContent>
<div className="h-64">
<ResponsiveContainer width="100%" height="100%">
<BarChart data={programmePerformanceData}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="name" />
<YAxis />
<Tooltip />
<Bar dataKey="completion" fill="#3b82f6" name="Completion Rate (%)" />
</BarChart>
</ResponsiveContainer>
</div>
</CardContent>
</Card>
{/* Learner Status Distribution */}
<Card>
<CardHeader>
<CardTitle>Learner Status</CardTitle>
<CardDescription>Distribution by current status</CardDescription>
</CardHeader>
<CardContent>
<div className="h-64">
<ResponsiveContainer width="100%" height="100%">
<RePieChart>
<Pie
data={learnerStatusData}
cx="50%"
cy="50%"
innerRadius={60}
outerRadius={80}
paddingAngle={5}
dataKey="value"
>
{learnerStatusData.map((entry, index) => (
<Cell key={`cell-${index}`} fill={entry.color} />
))}
</Pie>
<Tooltip />
<Legend />
</RePieChart>
</ResponsiveContainer>
</div>
</CardContent>
</Card>
</div>
</TabsContent>
{/* Programmes Tab */}
<TabsContent value="programmes" className="space-y-4 mt-6">
{programmePerformanceData.map(programme => (
<Card key={programme.name}>
<CardContent className="pt-6">
<div className="flex items-center justify-between mb-4">
<div>
<h3 className="font-semibold">{programme.name}</h3>
<p className="text-sm text-muted-foreground">
{programme.enrollment} enrolled learners
</p>
</div>
<Badge variant="outline">
{programme.completion}% Completion
</Badge>
</div>
<div className="space-y-3">
<div>
<div className="flex justify-between text-sm mb-1">
<span>Completion Progress</span>
<span className="font-medium">{programme.completion}%</span>
</div>
<Progress value={programme.completion} className="h-2" />
</div>
<div className="grid grid-cols-3 gap-4 pt-2">
<div>
<p className="text-xs text-muted-foreground">Active</p>
<p className="text-lg font-semibold">
{Math.floor(programme.enrollment * 0.7)}
</p>
</div>
<div>
<p className="text-xs text-muted-foreground">Completed</p>
<p className="text-lg font-semibold">
{Math.floor(programme.enrollment * 0.25)}
</p>
</div>
<div>
<p className="text-xs text-muted-foreground">At Risk</p>
<p className="text-lg font-semibold">
{Math.floor(programme.enrollment * 0.05)}
</p>
</div>
</div>
</div>
</CardContent>
</Card>
))}
</TabsContent>
{/* Learners Tab */}
<TabsContent value="learners" className="space-y-4 mt-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{/* Top Performers */}
<Card>
<CardHeader>
<CardTitle>Top Performers</CardTitle>
<CardDescription>Learners with highest completion rates</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-3">
{[1, 2, 3, 4, 5].map(i => (
<div key={i} className="flex items-center justify-between p-2 bg-muted/30 rounded-lg">
<div className="flex items-center gap-3">
<div className="w-8 h-8 bg-primary/10 rounded-full flex items-center justify-center">
<span className="text-sm font-medium">JD</span>
</div>
<div>
<p className="font-medium">John Doe</p>
<p className="text-xs text-muted-foreground">Leadership Program</p>
</div>
</div>
<Badge variant="default">98%</Badge>
</div>
))}
</div>
</CardContent>
</Card>
{/* At Risk Learners */}
<Card>
<CardHeader>
<CardTitle>At Risk Learners</CardTitle>
<CardDescription>Learners needing intervention</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-3">
{[1, 2, 3, 4, 5].map(i => (
<div key={i} className="flex items-center justify-between p-2 bg-red-50 rounded-lg">
<div className="flex items-center gap-3">
<div className="w-8 h-8 bg-red-100 rounded-full flex items-center justify-center">
<span className="text-sm font-medium text-red-600">JS</span>
</div>
<div>
<p className="font-medium">Jane Smith</p>
<p className="text-xs text-muted-foreground">Technical Skills</p>
</div>
</div>
<Badge variant="destructive">32%</Badge>
</div>
))}
</div>
</CardContent>
</Card>
</div>
{/* Learner Activity Table */}
<Card>
<CardHeader>
<CardTitle>Recent Learner Activity</CardTitle>
<CardDescription>Last 7 days of engagement</CardDescription>
</CardHeader>
<CardContent>
<div className="rounded-md border">
<table className="w-full">
<thead className="bg-muted/50">
<tr>
<th className="p-3 text-left text-sm font-medium">Learner</th>
<th className="p-3 text-left text-sm font-medium">Programme</th>
<th className="p-3 text-left text-sm font-medium">Progress</th>
<th className="p-3 text-left text-sm font-medium">Last Activity</th>
<th className="p-3 text-left text-sm font-medium">Status</th>
</tr>
</thead>
<tbody>
{[1, 2, 3, 4, 5].map(i => (
<tr key={i} className="border-t">
<td className="p-3">Sarah Chen</td>
<td className="p-3">Leadership</td>
<td className="p-3">
<div className="flex items-center gap-2">
<Progress value={85} className="w-16" />
<span>85%</span>
</div>
</td>
<td className="p-3">2 hours ago</td>
<td className="p-3">
<Badge>Active</Badge>
</td>
</tr>
))}
</tbody>
</table>
</div>
</CardContent>
</Card>
</TabsContent>
{/* Engagement Tab */}
<TabsContent value="engagement" className="space-y-6 mt-6">
{/* Activity Heatmap */}
<Card>
<CardHeader>
<CardTitle>Weekly Activity Pattern</CardTitle>
<CardDescription>Learning activity by day and type</CardDescription>
</CardHeader>
<CardContent>
<div className="h-64">
<ResponsiveContainer width="100%" height="100%">
<BarChart data={activityData}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="day" />
<YAxis />
<Tooltip />
<Legend />
<Bar dataKey="video" fill="#3b82f6" name="Video" />
<Bar dataKey="reading" fill="#22c55e" name="Reading" />
<Bar dataKey="quiz" fill="#eab308" name="Quiz" />
</BarChart>
</ResponsiveContainer>
</div>
</CardContent>
</Card>
{/* Engagement Metrics */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<Card>
<CardContent className="pt-6">
<div className="text-center">
<p className="text-3xl font-bold">85%</p>
<p className="text-sm text-muted-foreground">Video Completion Rate</p>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="pt-6">
<div className="text-center">
<p className="text-3xl font-bold">4.2</p>
<p className="text-sm text-muted-foreground">Avg. Hours/Week</p>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="pt-6">
<div className="text-center">
<p className="text-3xl font-bold">1,247</p>
<p className="text-sm text-muted-foreground">Forum Posts</p>
</div>
</CardContent>
</Card>
</div>
</TabsContent>
</Tabs>
</CardContent>
</Card>
{/* Export Options */}
<Card>
<CardHeader>
<CardTitle>Export Options</CardTitle>
<CardDescription>Download reports in various formats</CardDescription>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<Button
variant="outline"
className="h-auto py-4 flex-col gap-2"
onClick={() => handleExport('excel')}
>
<FileText className="h-6 w-6" />
<span>Excel Report</span>
<span className="text-xs text-muted-foreground">.xlsx with charts</span>
</Button>
<Button
variant="outline"
className="h-auto py-4 flex-col gap-2"
onClick={() => handleExport('pdf')}
>
<FileText className="h-6 w-6" />
<span>PDF Report</span>
<span className="text-xs text-muted-foreground">Formatted summary</span>
</Button>
<Button
variant="outline"
className="h-auto py-4 flex-col gap-2"
onClick={() => handleExport('csv')}
>
<TableIcon className="h-6 w-6" />
<span>CSV Data</span>
<span className="text-xs text-muted-foreground">Raw data export</span>
</Button>
</div>
</CardContent>
</Card>
</div>
);
};
export default ReportsPage;

53
src/routes/index.tsx Normal file
View File

@@ -0,0 +1,53 @@
import { createBrowserRouter, Navigate } from 'react-router-dom';
import HRLayout from '../layouts/HRLayout';
import DashboardPage from '../pages/Dashboard/DashboardPage';
import LearnersPage from '../pages/Learners/LearnersPage';
import ReportsPage from '../pages/ReportsPage/ReportsPage';
import DiscussionsPage from '../pages/DiscussionsPage/DiscussionsPage';
import ProgrammeViewPage from '../pages/ProgrammeViewPage/ProgrammeViewPage';
import CourseViewPage from '../pages/CourseViewPage/CourseViewPage';
export const router = createBrowserRouter([
{
path: '/',
element: <Navigate to="/hr/dashboard" replace />,
},
{
path: '/hr',
element: <HRLayout />,
children: [
{
index: true,
element: <Navigate to="dashboard" replace />,
},
{
path: 'dashboard',
element: <DashboardPage />,
},
{
path: 'learners',
element: <LearnersPage />,
},
{
path: 'reports',
element: <ReportsPage />,
},
{
path: 'discussions',
element: <DiscussionsPage />,
},
{
path: 'programme/:programmeId',
element: <ProgrammeViewPage />,
},
{
path: 'course/:courseId',
element: <CourseViewPage />,
},
{
path: 'profile',
element: <DashboardPage />, // You can create a separate ProfilePage later
},
],
},
]);

135
src/types/index.ts Normal file
View File

@@ -0,0 +1,135 @@
export interface KPIData {
title: string;
value: number;
change?: number;
trend?: 'up' | 'down' | 'neutral';
}
export interface Employee {
id: string;
name: string;
email: string;
phone: string;
status: 'Active' | 'Inactive' | 'Pending';
programme?: string;
course?: string;
progress?: number;
lastActivity?: string;
}
export interface Announcement {
id: string;
title: string;
content: string;
type: 'announcement' | 'reminder';
timestamp: string;
pinned?: boolean;
}
export interface Deadline {
id: string;
title: string;
type: 'webinar' | 'profiler';
dueDate: string;
dueTime: string;
}
export interface Cohort {
id: string;
name: string;
memberCount: number;
programme?: string;
isActive: boolean;
}
export interface Thread {
id: string;
title: string;
content: string;
author: {
id: string;
name: string;
avatar?: string;
};
cohortId: string;
createdAt: string;
lastActivity: string;
replyCount: number;
isPinned?: boolean;
tags?: string[];
reactions?: { [key: string]: string[] };
}
export interface Post {
id: string;
threadId: string;
content: string;
author: {
id: string;
name: string;
avatar?: string;
};
createdAt: string;
editedAt?: string;
reactions?: { [key: string]: string[] };
isReported?: boolean;
parentId?: string;
}
export interface TestimonialFormData {
name: string;
email: string;
phone: string;
organisation: string;
programme: string;
testimonialText: string;
consentToPublish: boolean;
}
export interface Programme {
programmeId: string;
title: string;
status: 'Active' | 'Upcoming' | 'Completed';
coursesCount: number;
contentCount: number;
assignment: {
startDate: Date;
endDate: Date;
};
learnersAssigned: number;
}
export interface Course {
id: string;
title: string;
status: 'Published' | 'Draft' | 'Archived';
code: string;
owner: string;
version: number;
duration: string;
description: string;
objectives: string[];
tags: string[];
modules: CourseModule[];
linkedProgrammes: LinkedProgramme[];
}
export interface CourseModule {
id: string;
title: string;
lessons: CourseLesson[];
}
export interface CourseLesson {
id: string;
title: string;
type: 'video' | 'quiz' | 'read' | 'assignment';
eta: string;
dueDate?: string;
status?: 'Not Started' | 'In Progress' | 'Completed';
}
export interface LinkedProgramme {
id: string;
title: string;
}

235
src/utils/mockData.ts Normal file
View File

@@ -0,0 +1,235 @@
import { Employee, Announcement, Deadline, Cohort, Thread, Post, Programme, Course } from '../types';
export const mockKPIData = [
{ title: 'Total Learners', value: 1247, change: 12, trend: 'up' as const },
{ title: 'Active Courses', value: 89, change: 5, trend: 'up' as const },
{ title: 'Completed Profilers', value: 342, change: -8, trend: 'down' as const },
{ title: 'Average Progress', value: 73, change: 7, trend: 'up' as const }
];
export const mockEmployees: Employee[] = [
{ id: '1', name: 'Sarah Chen', email: 'sarah.chen@company.com', phone: '+61 4XX XXX XXX', status: 'Active', programme: 'Leadership Development', course: 'Strategic Thinking', progress: 85, lastActivity: '2 hours ago' },
{ id: '2', name: 'Michael Rodriguez', email: 'michael.r@company.com', phone: '+61 4XX XXX XXX', status: 'Active', programme: 'Technical Skills', course: 'Data Analysis', progress: 62, lastActivity: '1 day ago' },
{ id: '3', name: 'Emma Thompson', email: 'emma.thompson@company.com', phone: '+61 4XX XXX XXX', status: 'Pending', programme: 'Communication', course: 'Public Speaking', progress: 0, lastActivity: 'Never' },
{ id: '4', name: 'David Kim', email: 'david.kim@company.com', phone: '+61 4XX XXX XXX', status: 'Active', programme: 'Project Management', course: 'Agile Methodology', progress: 94, lastActivity: '3 hours ago' },
{ id: '5', name: 'Lisa Wang', email: 'lisa.wang@company.com', phone: '+61 4XX XXX XXX', status: 'Active', programme: 'Leadership Development', course: 'Team Management', progress: 78, lastActivity: '5 hours ago' },
{ id: '6', name: 'James Wilson', email: 'james.wilson@company.com', phone: '+61 4XX XXX XXX', status: 'Inactive', programme: 'Technical Skills', course: 'Programming Basics', progress: 34, lastActivity: '2 weeks ago' },
{ id: '7', name: 'Maria Garcia', email: 'maria.garcia@company.com', phone: '+61 4XX XXX XXX', status: 'Active', programme: 'Sales Training', course: 'Customer Relations', progress: 56, lastActivity: '1 day ago' },
{ id: '8', name: 'Robert Lee', email: 'robert.lee@company.com', phone: '+61 4XX XXX XXX', status: 'Active', programme: 'Leadership Development', course: 'Decision Making', progress: 89, lastActivity: '4 hours ago' },
{ id: '9', name: 'Jennifer Davis', email: 'jennifer.davis@company.com', phone: '+61 4XX XXX XXX', status: 'Pending', programme: 'Communication', course: 'Written Communication', progress: 0, lastActivity: 'Never' },
{ id: '10', name: 'Thomas Brown', email: 'thomas.brown@company.com', phone: '+61 4XX XXX XXX', status: 'Active', programme: 'Project Management', course: 'Risk Management', progress: 71, lastActivity: '6 hours ago' }
];
export const mockAnnouncements: Announcement[] = [
{ id: '1', title: 'New Learning Module Available', content: 'Advanced Analytics course is now live in the system.', type: 'announcement', timestamp: '2 hours ago', pinned: true },
{ id: '2', title: 'Reminder: Quarterly Reviews Due', content: 'Please complete all quarterly progress reviews by Friday.', type: 'reminder', timestamp: '5 hours ago' },
{ id: '3', title: 'System Maintenance Scheduled', content: 'Learning platform will be offline Saturday 2-4 AM for updates.', type: 'announcement', timestamp: '1 day ago' }
];
export const mockDeadlines: Deadline[] = [
{ id: '1', title: 'Leadership Webinar Series', type: 'webinar', dueDate: 'Today', dueTime: '2:00 PM' },
{ id: '2', title: 'Communication Skills Assessment', type: 'profiler', dueDate: 'Tomorrow', dueTime: '11:59 PM' },
{ id: '3', title: 'Project Management Workshop', type: 'webinar', dueDate: 'Dec 30', dueTime: '10:00 AM' },
{ id: '4', title: 'Technical Skills Profiler', type: 'profiler', dueDate: 'Jan 2', dueTime: '5:00 PM' },
{ id: '5', title: 'Team Building Session', type: 'webinar', dueDate: 'Jan 5', dueTime: '3:30 PM' }
];
export const mockCohorts: Cohort[] = [
{ id: '1', name: 'Leadership Development Q4 2024', memberCount: 30, programme: 'Leadership Development', isActive: true },
{ id: '2', name: 'Technical Skills Cohort A', memberCount: 25, programme: 'Technical Skills', isActive: true },
{ id: '3', name: 'Communication Workshop Group', memberCount: 18, programme: 'Communication', isActive: true },
{ id: '4', name: 'Project Management Certification', memberCount: 22, programme: 'Project Management', isActive: false }
];
export const mockThreads: Thread[] = [
{
id: '1',
title: 'Best practices for team communication during remote work',
content: 'What strategies have you found most effective for maintaining clear communication with remote team members? I\'d love to hear about tools and techniques that have worked well for your teams.',
author: { id: 'user1', name: 'Sarah Chen' },
cohortId: '1',
createdAt: '2024-12-28T10:30:00Z',
lastActivity: '2024-12-28T15:45:00Z',
replyCount: 12,
isPinned: true,
tags: ['communication', 'remote-work', 'best-practices'],
reactions: { '👍': ['user2', 'user3'], '💡': ['user4'] }
},
{
id: '2',
title: 'How to handle difficult conversations with team members?',
content: 'I\'m struggling with addressing performance issues with one of my team members. Any advice on how to approach this sensitively while being direct about expectations?',
author: { id: 'user2', name: 'Michael Rodriguez' },
cohortId: '1',
createdAt: '2024-12-28T09:15:00Z',
lastActivity: '2024-12-28T14:20:00Z',
replyCount: 8,
tags: ['difficult-conversations', 'performance-management'],
reactions: { '🤔': ['user1', 'user5'], '💪': ['user3'] }
},
{
id: '3',
title: 'Share your leadership book recommendations',
content: 'What books have been most influential in your leadership journey? Looking for practical reads that offer actionable insights.',
author: { id: 'user3', name: 'Emma Thompson' },
cohortId: '1',
createdAt: '2024-12-27T16:00:00Z',
lastActivity: '2024-12-28T11:30:00Z',
replyCount: 15,
tags: ['books', 'recommendations', 'learning'],
reactions: { '📚': ['user1', 'user2', 'user4', 'user5'], '⭐': ['user6'] }
},
{
id: '4',
title: 'Question about the delegation framework from Module 3',
content: 'Can someone clarify the difference between the delegation levels we covered? I want to make sure I\'m applying them correctly in my current projects.',
author: { id: 'user4', name: 'David Kim' },
cohortId: '1',
createdAt: '2024-12-27T14:30:00Z',
lastActivity: '2024-12-27T18:45:00Z',
replyCount: 6,
tags: ['module-3', 'delegation', 'clarification'],
reactions: { '❓': ['user2'], '👍': ['user1'] }
}
];
export const mockPosts: Post[] = [
{
id: '1',
threadId: '1',
content: 'Great question! I\'ve found that establishing clear communication protocols at the start of projects makes a huge difference. We use a combination of daily stand-ups via video call and async updates through Slack.',
author: { id: 'user5', name: 'Lisa Wang' },
createdAt: '2024-12-28T11:00:00Z',
reactions: { '👍': ['user1', 'user2'], '💯': ['user3'] }
},
{
id: '2',
threadId: '1',
content: 'One thing that\'s worked well for our team is having "communication preferences" documented for each team member. Some prefer quick calls for complex topics, others prefer detailed written explanations. Knowing this upfront prevents a lot of miscommunication.',
author: { id: 'user6', name: 'Robert Lee' },
createdAt: '2024-12-28T12:15:00Z',
reactions: { '💡': ['user1', 'user4'], '👏': ['user2'] }
},
{
id: '3',
threadId: '1',
content: '@Sarah Chen Thanks for starting this discussion! I\'d add that regular one-on-ones have been crucial for me. Even in remote settings, that personal connection makes a big difference in team dynamics.',
author: { id: 'user7', name: 'Jennifer Davis' },
createdAt: '2024-12-28T13:30:00Z',
reactions: { '🎯': ['user1'], '👍': ['user5'] }
}
];
export const mockProgrammes: Programme[] = [
{
programmeId: 'prog-001',
title: 'Leadership Development Program',
status: 'Active',
coursesCount: 8,
contentCount: 24,
assignment: {
startDate: new Date('2024-01-15'),
endDate: new Date('2024-06-30')
},
learnersAssigned: 45
},
{
programmeId: 'prog-002',
title: 'Technical Skills Bootcamp',
status: 'Active',
coursesCount: 12,
contentCount: 36,
assignment: {
startDate: new Date('2024-02-01'),
endDate: new Date('2024-08-31')
},
learnersAssigned: 38
},
{
programmeId: 'prog-003',
title: 'Communication Excellence',
status: 'Upcoming',
coursesCount: 6,
contentCount: 18,
assignment: {
startDate: new Date('2024-03-01'),
endDate: new Date('2024-05-31')
},
learnersAssigned: 28
},
{
programmeId: 'prog-004',
title: 'Project Management Certification',
status: 'Active',
coursesCount: 10,
contentCount: 30,
assignment: {
startDate: new Date('2024-01-01'),
endDate: new Date('2024-12-31')
},
learnersAssigned: 52
},
{
programmeId: 'prog-005',
title: 'Digital Marketing Mastery',
status: 'Completed',
coursesCount: 5,
contentCount: 15,
assignment: {
startDate: new Date('2023-09-01'),
endDate: new Date('2023-12-31')
},
learnersAssigned: 32
}
];
export const mockCourse: Course = {
id: 'crs_456',
title: 'Strategic Thinking and Decision Making',
status: 'Published',
code: 'STDM-2024',
owner: 'Prof. Michael Chen',
version: 1,
duration: '6 hours',
description: 'This course develops strategic thinking capabilities and decision-making frameworks for leaders at all levels. Participants will learn to analyze complex situations, evaluate options, and make informed decisions.',
objectives: [
'Apply strategic thinking frameworks to business challenges',
'Develop systematic approaches to decision making',
'Evaluate risks and opportunities effectively',
'Create actionable strategic plans'
],
tags: ['Strategy', 'Leadership', 'Decision Making', 'Critical Thinking'],
modules: [
{
id: 'm1',
title: 'Foundations of Strategic Thinking',
lessons: [
{ id: 'l1', title: 'Introduction to Strategic Thinking', type: 'video', eta: '15 mins', status: 'Completed' },
{ id: 'l2', title: 'Strategic Frameworks Overview', type: 'read', eta: '20 mins', status: 'Completed' },
{ id: 'l3', title: 'Knowledge Check', type: 'quiz', eta: '10 mins', status: 'In Progress' }
]
},
{
id: 'm2',
title: 'Decision Making Models',
lessons: [
{ id: 'l4', title: 'Rational Decision Making', type: 'video', eta: '25 mins', status: 'Not Started' },
{ id: 'l5', title: 'Intuitive vs Analytical Approaches', type: 'read', eta: '15 mins', status: 'Not Started' },
{ id: 'l6', title: 'Case Study Analysis', type: 'assignment', eta: '45 mins', dueDate: '2024-01-25', status: 'Not Started' }
]
},
{
id: 'm3',
title: 'Risk Assessment and Management',
lessons: [
{ id: 'l7', title: 'Risk Identification Techniques', type: 'video', eta: '20 mins', status: 'Not Started' },
{ id: 'l8', title: 'Risk Matrix and Evaluation', type: 'read', eta: '25 mins', status: 'Not Started' },
{ id: 'l9', title: 'Final Assessment', type: 'quiz', eta: '30 mins', dueDate: '2024-01-30', status: 'Not Started' }
]
}
],
linkedProgrammes: [
{ id: 'prg_123', title: 'Executive Leadership Development Programme' },
{ id: 'prg_124', title: 'Management Excellence Programme' }
]
};