need to fix layout
This commit is contained in:
58
package-lock.json
generated
58
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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": "*",
|
||||
|
||||
3846
src/App.tsx
3846
src/App.tsx
File diff suppressed because it is too large
Load Diff
2343
src/App_new.tsx
2343
src/App_new.tsx
File diff suppressed because it is too large
Load Diff
79
src/components/BreadcrumbNav.tsx
Normal file
79
src/components/BreadcrumbNav.tsx
Normal 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
308
src/components/TopNav.tsx
Normal 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;
|
||||
69
src/components/shared/ChatBot.tsx
Normal file
69
src/components/shared/ChatBot.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
84
src/components/shared/KPICard.tsx
Normal file
84
src/components/shared/KPICard.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -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
23
src/hooks/useCountUp.ts
Normal 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;
|
||||
}
|
||||
24
src/hooks/useLocalStorage.ts
Normal file
24
src/hooks/useLocalStorage.ts
Normal 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
105
src/layouts/HRLayout.tsx
Normal 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>© {new Date().getFullYear()} Knowledge Learning Centre. All rights reserved.</p>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default HRLayout;
|
||||
63
src/layouts/components/BreadcrumbNav.tsx
Normal file
63
src/layouts/components/BreadcrumbNav.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
69
src/layouts/components/HRSidebar.tsx
Normal file
69
src/layouts/components/HRSidebar.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
15
src/main.tsx
15
src/main.tsx
@@ -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>
|
||||
);
|
||||
503
src/pages/CourseViewPage/CourseViewPage.tsx
Normal file
503
src/pages/CourseViewPage/CourseViewPage.tsx
Normal 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;
|
||||
164
src/pages/Dashboard/DashboardPage.tsx
Normal file
164
src/pages/Dashboard/DashboardPage.tsx
Normal 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;
|
||||
710
src/pages/DiscussionsPage/DiscussionsPage.tsx
Normal file
710
src/pages/DiscussionsPage/DiscussionsPage.tsx
Normal 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;
|
||||
823
src/pages/Learners/LearnersPage.tsx
Normal file
823
src/pages/Learners/LearnersPage.tsx
Normal 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;
|
||||
519
src/pages/ProgrammeViewPage/ProgrammeViewPage.tsx
Normal file
519
src/pages/ProgrammeViewPage/ProgrammeViewPage.tsx
Normal 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;
|
||||
574
src/pages/ReportsPage/ReportsPage.tsx
Normal file
574
src/pages/ReportsPage/ReportsPage.tsx
Normal 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
53
src/routes/index.tsx
Normal 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
135
src/types/index.ts
Normal 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
235
src/utils/mockData.ts
Normal 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' }
|
||||
]
|
||||
};
|
||||
Reference in New Issue
Block a user