diff --git a/package-lock.json b/package-lock.json
index 5bcb44c..764e1f2 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -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",
diff --git a/package.json b/package.json
index 909892d..9c91917 100644
--- a/package.json
+++ b/package.json
@@ -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": "*",
diff --git a/src/App.tsx b/src/App.tsx
index 7360e3d..f351504 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1,3843 +1,9 @@
-import React, { useState, useEffect, useCallback } from 'react';
-import { Button } from './components/ui/button';
-import { ProgrammesTable } from './components/ProgrammesTable';
-import { ProgrammeSchedule } from './components/ProgrammeSchedule';
-import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, DropdownMenuSeparator, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger } from './components/ui/dropdown-menu';
-import klcLogo from './assets/klc-logo.png';
-import { ProgrammeCalendar } from './components/ProgrammeCalendar';
-import { LearningAnalyticsTable } from './components/LearningAnalyticsTable';
-import { DiscussionForumFeed } from './components/DiscussionForumFeed';
-import { ProgrammeHRView } from './components/ProgrammeHRView';
-import { CourseHRView } from './components/CourseHRView';
-import { Card, CardContent, CardDescription, CardHeader, CardTitle } from './components/ui/card';
-import { Badge } from './components/ui/badge';
-import { Input } from './components/ui/input';
-import { Textarea } from './components/ui/textarea';
-import { Checkbox } from './components/ui/checkbox';
-import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './components/ui/select';
-import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from './components/ui/dialog';
-import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle, SheetTrigger } from './components/ui/sheet';
-import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from './components/ui/table';
-import { Progress } from './components/ui/progress';
-import { Skeleton } from './components/ui/skeleton';
-import { Tabs, TabsContent, TabsList, TabsTrigger } from './components/ui/tabs';
-import { Breadcrumb, BreadcrumbItem, BreadcrumbLink, BreadcrumbList, BreadcrumbPage, BreadcrumbSeparator } from './components/ui/breadcrumb';
-import { Alert, AlertDescription } from './components/ui/alert';
-import { Pagination, PaginationContent, PaginationItem, PaginationLink, PaginationNext, PaginationPrevious, PaginationEllipsis } from './components/ui/pagination';
-import {
- Home,
- Users,
- BarChart3,
- MessageSquare,
- Settings,
- Search,
- Filter,
- Download,
- Plus,
- Upload,
- Edit,
- MoreHorizontal,
- TrendingUp,
- Calendar,
- Clock,
- BookOpen,
- Award,
- Bell,
- ChevronDown,
- Menu,
- X,
- ArrowLeft,
- ChevronRight,
- RefreshCw,
- CheckCircle,
- AlertCircle,
- XCircle,
- FileText,
- Mail,
- Phone,
- Eye,
- Trash2,
- Building2,
- CreditCard,
- Shield,
- ExternalLink,
- Info,
- LogOut,
- User,
- Repeat,
- Users2,
- Pin,
- Heart,
- MessageCircle,
- Share2,
- Flag,
- Reply,
- Hash,
- Send,
- Image,
- Bold,
- Italic,
- Link,
- List,
- Smile
-} from 'lucide-react';
-import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, AreaChart, Area } from 'recharts';
+import React from 'react';
+import { RouterProvider } from 'react-router-dom';
+import { router } from './routes';
-// Types
-interface KPIData {
- title: string;
- value: number;
- change?: number;
- trend?: 'up' | 'down' | 'neutral';
+function App() {
+ return ;
}
-interface Employee {
- id: string;
- name: string;
- email: string;
- phone: string;
- status: 'Active' | 'Inactive' | 'Pending';
- programme?: string;
- course?: string;
- progress?: number;
- lastActivity?: string;
-}
-
-interface Announcement {
- id: string;
- title: string;
- content: string;
- type: 'announcement' | 'reminder';
- timestamp: string;
- pinned?: boolean;
-}
-
-interface Deadline {
- id: string;
- title: string;
- type: 'webinar' | 'profiler';
- dueDate: string;
- dueTime: string;
-}
-
-interface Cohort {
- id: string;
- name: string;
- memberCount: number;
- programme?: string;
- isActive: boolean;
-}
-
-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[] }; // emoji -> user IDs
-}
-
-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; // for nested replies
-}
-
-interface TestimonialFormData {
- name: string;
- email: string;
- phone: string;
- organisation: string;
- programme: string;
- testimonialText: string;
- consentToPublish: boolean;
-}
-
-// Helper function to determine user access level
-// This would typically be determined by user roles/permissions from your backend
-const getUserAccessLevel = (): 'full' | 'course-only' => {
- // Mock logic: For demonstration, we'll simulate that users with email containing 'course-only'
- // have course-only access. In real implementation, this would check user roles/permissions.
-
- // For testing purposes, you can change this return value:
- // - 'full': Shows programmes with "Programme/Course" column header and programme data
- // - 'course-only': Shows only courses with "Course" column header and course data
-
- // TODO: Replace with actual user role/permission check from your backend
- // Example: return userRole === 'hr-course-only' ? 'course-only' : 'full';
- return 'full'; // Default to full access
-};
-
-// Custom hooks
-const useLocalStorage = (key: string, initialValue: any) => {
- const [storedValue, setStoredValue] = useState(() => {
- try {
- const item = window.localStorage.getItem(key);
- return item ? JSON.parse(item) : initialValue;
- } catch (error) {
- return initialValue;
- }
- });
-
- const setValue = (value: any) => {
- try {
- setStoredValue(value);
- window.localStorage.setItem(key, JSON.stringify(value));
- } catch (error) {
- console.error('Error saving to localStorage:', error);
- }
- };
-
- return [storedValue, setValue];
-};
-
-const 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;
-};
-
-// Mock data
-const mockKPIData: KPIData[] = [
- { title: 'Total Learners', value: 1247, change: 12, trend: 'up' },
- { title: 'Active Courses', value: 89, change: 5, trend: 'up' },
- { title: 'Completed Profilers', value: 342, change: -8, trend: 'down' },
- { title: 'Average Progress', value: 73, change: 7, trend: 'up' }
-];
-
-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' }
-];
-
-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' }
-];
-
-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' }
-];
-
-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 }
-];
-
-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'] }
- }
-];
-
-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'] }
- }
-];
-
-// Chart data for reports
-const completionTrendsData = [
- { week: 'Week 1', completed: 12, started: 25, completionRate: 48 },
- { week: 'Week 2', completed: 18, started: 30, completionRate: 60 },
- { week: 'Week 3', completed: 22, started: 35, completionRate: 63 },
- { week: 'Week 4', completed: 28, started: 40, completionRate: 70 },
- { week: 'Week 5', completed: 35, started: 45, completionRate: 78 },
- { week: 'Week 6', completed: 42, started: 50, completionRate: 84 },
- { week: 'Week 7', completed: 38, started: 48, completionRate: 79 },
- { week: 'Week 8', completed: 45, started: 52, completionRate: 87 },
- { week: 'Week 9', completed: 48, started: 55, completionRate: 87 },
- { week: 'Week 10', completed: 52, started: 58, completionRate: 90 },
- { week: 'Week 11', completed: 55, started: 60, completionRate: 92 },
- { week: 'Week 12', completed: 58, started: 62, completionRate: 94 }
-];
-
-const activityHeatmapData = [
- { day: 'Mon', '6AM': 2, '9AM': 15, '12PM': 25, '3PM': 35, '6PM': 20, '9PM': 8 },
- { day: 'Tue', '6AM': 3, '9AM': 18, '12PM': 30, '3PM': 38, '6PM': 22, '9PM': 10 },
- { day: 'Wed', '6AM': 5, '9AM': 22, '12PM': 28, '3PM': 42, '6PM': 25, '9PM': 12 },
- { day: 'Thu', '6AM': 4, '9AM': 20, '12PM': 32, '3PM': 40, '6PM': 28, '9PM': 15 },
- { day: 'Fri', '6AM': 6, '9AM': 25, '12PM': 35, '3PM': 30, '6PM': 18, '9PM': 8 },
- { day: 'Sat', '6AM': 8, '9AM': 12, '12PM': 20, '3PM': 25, '6PM': 30, '9PM': 22 },
- { day: 'Sun', '6AM': 10, '9AM': 8, '12PM': 15, '3PM': 18, '6PM': 28, '9PM': 25 }
-];
-
-// Components
-const KPICard: React.FC<{ data: KPIData; onClick?: () => void; className?: string }> = ({ data, onClick, className = '' }) => {
- const countedValue = useCountUp(data.value);
- const [prefersReducedMotion] = useLocalStorage('prefersReducedMotion', false);
-
- return (
- {
- if (e.key === 'Enter' || e.key === ' ') {
- e.preventDefault();
- onClick?.();
- }
- }}
- >
-
- {data.title}
-
-
-
-
- {prefersReducedMotion ? data.value : countedValue}
- {data.title.includes('Progress') && '%'}
-
- {data.change && (
-
- {data.trend === 'up' ? '+' : ''}{data.change}{data.title.includes('Progress') ? '%' : ''}
-
- )}
-
-
-
- );
-};
-
-const EmployeeProgressCard: React.FC<{
- employee: Employee;
- onEdit?: (employee: Employee) => void;
-}> = ({ employee, onEdit }) => {
- const getProgressColor = (progress: number) => {
- if (progress >= 80) return 'bg-status-success';
- if (progress >= 60) return 'bg-status-warn';
- return 'bg-status-error';
- };
-
- const getProgressStatus = (progress: number) => {
- if (progress === 0) return 'Not Started';
- if (progress >= 80) return 'Excellent';
- if (progress >= 60) return 'Good';
- return 'Needs Attention';
- };
-
- return (
- onEdit?.(employee)}>
-
-
-
-
- {employee.name.split(' ').map(n => n[0]).join('')}
-
-
-
{employee.name}
-
- {employee.status}
-
-
-
-
{ e.stopPropagation(); onEdit?.(employee); }}>
-
-
-
-
- {employee.programme && (
-
-
-
-
{employee.programme}
-
{employee.course}
-
-
{employee.progress}%
-
-
-
-
-
-
- {getProgressStatus(employee.progress || 0)}
-
- {employee.lastActivity}
-
-
-
- )}
-
-
- );
-};
-
-const EmployeeTable: React.FC<{
- employees: Employee[];
- onEdit?: (employee: Employee) => void;
- showProgress?: boolean;
- maxHeight?: string;
- viewMode?: 'table' | 'cards';
-}> = ({ employees, onEdit, showProgress = true, maxHeight = "400px", viewMode = 'table' }) => {
- if (viewMode === 'cards') {
- return (
-
- {employees.map((employee) => (
-
- ))}
-
- );
- }
-
- return (
-
-
-
-
- Employee
- Email
- Phone
- Status
- {showProgress && (
- <>
- Programme/Course
- Progress
- Last Activity
- >
- )}
- Actions
-
-
-
- {employees.map((employee) => (
- onEdit?.(employee)}
- role="button"
- tabIndex={0}
- onKeyDown={(e) => {
- if (e.key === 'Enter' || e.key === ' ') {
- e.preventDefault();
- onEdit?.(employee);
- }
- }}
- >
- {employee.name}
- {employee.email}
- {employee.phone}
-
-
- {employee.status}
-
-
- Employee status is {employee.status}
-
-
- {showProgress && (
- <>
-
-
-
{employee.programme}
-
{employee.course}
-
-
-
- {employee.progress !== undefined && (
-
-
-
{employee.progress}%
-
- Progress: {employee.progress} percent complete
-
-
- )}
-
- {employee.lastActivity}
- >
- )}
-
- {
- e.stopPropagation();
- onEdit?.(employee);
- }}
- aria-label={`Edit ${employee.name}`}
- >
-
-
-
-
- ))}
-
-
-
- );
-};
-
-const HRSidebar: React.FC<{
- activeScreen: string;
- onNavigate: (screen: string) => void;
- className?: string;
-}> = ({ activeScreen, onNavigate, className = '' }) => {
- const [prefersReducedMotion] = useLocalStorage('prefersReducedMotion', false);
-
- const menuItems = [
- { id: 'home', label: 'Dashboard', icon: Home, path: '/hr/home' },
- { 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' }
- ];
-
- return (
-
-
-
-
- TS
-
-
Tech Solutions Pvt Ltd
-
-
-
-
-
- {menuItems.map((item) => {
- const Icon = item.icon;
- const isActive = activeScreen === item.id;
-
- return (
-
- onNavigate(item.id)}
- className={`
- 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-current={isActive ? 'page' : undefined}
- aria-label={`Navigate to ${item.label}`}
- >
-
- {item.label}
-
-
- );
- })}
-
-
-
- );
-};
-
-const TopNav: React.FC<{
- onMenuToggle?: () => void;
- showMenuButton?: boolean;
- onNotificationToggle?: () => void;
- notificationCount?: number;
-}> = ({ onMenuToggle, showMenuButton = false, onNotificationToggle, notificationCount = 0 }) => {
- return (
-
- );
-};
-
-const BreadcrumbNav: React.FC<{ currentScreen: string; currentProgrammeId?: string | null; currentCourseId?: string | null }> = ({ currentScreen, currentProgrammeId, currentCourseId }) => {
- const getBreadcrumbText = (screen: string) => {
- switch (screen) {
- case 'home': return 'Dashboard';
- case 'learners': return 'Learners';
- case 'discussions': return 'Discussion Forums';
- case 'reports': return 'Reports';
- case 'profile': return 'Profile';
- case 'programme-view': return 'Programme Details';
- case 'course-view': return 'Course Details';
- default: return 'HR Portal';
- }
- };
-
- return (
-
-
-
- HR Portal
-
-
-
- {getBreadcrumbText(currentScreen)}
-
-
-
- );
-};
-
-const AnnouncementsPanel: React.FC<{
- isOpen: boolean;
- onClose: () => void;
- announcements: Announcement[];
- onMarkAsRead?: (id: string) => void;
-}> = ({ isOpen, onClose, announcements, onMarkAsRead }) => {
- if (!isOpen) return null;
-
- return (
-
-
-
Announcements & Reminders
-
-
-
-
-
-
- {announcements.map((item) => (
-
onMarkAsRead?.(item.id)}
- >
-
-
-
- {item.type === 'announcement' ?
- :
-
- }
-
- {item.pinned && (
-
Pinned
- )}
-
-
{item.timestamp}
-
-
-
{item.title}
-
{item.content}
-
-
-
- {item.type}
-
-
- Mark as read
-
-
-
- ))}
-
-
-
-
- View All Notifications
-
-
-
- );
-};
-
-const ChatBot: React.FC<{ currentScreen?: string }> = ({ 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 (
-
- {isOpen && (
-
-
-
HR Assistant
- setIsOpen(false)}
- className="h-6 w-6"
- aria-label="Close chat"
- >
-
-
-
-
- {chips.map((chip, index) => (
-
- {chip}
-
- ))}
-
-
- )}
-
setIsOpen(!isOpen)}
- className="rounded-full h-12 w-12 shadow-lg min-tap-44"
- aria-label="Open HR chat assistant"
- aria-expanded={isOpen}
- >
-
-
-
- );
-};
-
-// Screen Components
-const HRHomeScreen: React.FC<{
- onNavigate: (screen: string, filters?: any) => void;
- onViewProgramme: (programmeId: string) => void;
- onViewCourse: (courseId: string) => void;
-}> = ({ onNavigate, onViewProgramme, onViewCourse }) => {
- const [loading, setLoading] = useState(true);
- const [prefersReducedMotion] = useLocalStorage('prefersReducedMotion', false);
-
- useEffect(() => {
- const timer = setTimeout(() => setLoading(false), 800);
- return () => clearTimeout(timer);
- }, []);
-
- if (loading) {
- return (
-
-
- {[...Array(4)].map((_, i) => (
-
-
-
-
-
-
-
-
- ))}
-
-
-
-
-
-
-
-
-
-
- );
- }
-
- return (
-
- {/* Welcome Section */}
-
-
-
Welcome Priya 👋
-
Manage programmes, track progress, and stay connected with your learning community
-
-
-
- {/* Quick Actions Section - Unchanged */}
-
-
- Quick Actions
- Common HR tasks and shortcuts
-
-
-
- {[
- { title: 'Add Learners', icon: Plus, action: () => onNavigate('learners', { action: 'add' }) },
- { title: 'Assign', icon: BookOpen, action: () => onNavigate('learners', { action: 'assign' }) },
- { title: 'Download Reports', icon: Download, action: () => onNavigate('reports') },
- { title: 'Submit Testimonial', icon: MessageSquare, action: () => onNavigate('profile') }
- ].map((link, index) => {
- const Icon = link.icon;
- return (
-
-
- {link.title}
-
- );
- })}
-
-
-
-
- {/* Programmes Table */}
-
-
console.log(`Assign learners to: ${programmeId}`)}
- onDownloadTracker={(programmeId) => console.log(`Download tracker for: ${programmeId}`)}
- userAccessLevel={getUserAccessLevel()}
- />
-
-
- {/* Programme Schedule - Horizontal layout below programmes */}
-
-
console.log(`Open event: ${event.title}`)}
- />
-
-
- {/* Learning Analytics - Full Width */}
-
- onNavigate('learners', { editEmployee: learnerId })}
- onNudgeLearner={(learnerId) => console.log(`Nudge learner: ${learnerId}`)}
- onViewAllAnalytics={(programmeId) => onNavigate('reports', { programme: programmeId })}
- />
-
-
- {/* Discussion Forum Feed */}
-
- console.log(`Open thread: ${threadId}`)}
- onMarkAsRead={(threadId) => console.log(`Mark as read: ${threadId}`)}
- />
-
-
- );
-};
-
-const LearnersScreen: React.FC<{ filters?: any }> = ({ filters }) => {
- const [employees, setEmployees] = useState(mockEmployees);
- const [searchTerm, setSearchTerm] = useState('');
- const [statusFilter, setStatusFilter] = useState('all');
- const [selectedEmployees, setSelectedEmployees] = useState([]);
- const [showAddDrawer, setShowAddDrawer] = useState(false);
- const [showImportModal, setShowImportModal] = useState(false);
- const [showAssignModal, setShowAssignModal] = useState(false);
- const [showEditDrawer, setShowEditDrawer] = useState(false);
- const [editingEmployee, setEditingEmployee] = useState(null);
- const [newEmployee, setNewEmployee] = useState({ name: '', email: '', phone: '' });
- const [bulkActionVisible, setBulkActionVisible] = useState(false);
- const [currentPage, setCurrentPage] = useState(1);
- const [itemsPerPage, setItemsPerPage] = useState(10);
-
- const debouncedSearch = useCallback(
- (term: string) => {
- // Simulating search with 300ms delay
- 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;
- return matchesSearch && matchesStatus;
- });
-
- // 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]);
-
- 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([]); // Clear selections when changing pages
- };
-
- useEffect(() => {
- setBulkActionVisible(selectedEmployees.length > 0);
- }, [selectedEmployees]);
-
- 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'
- };
- setEmployees(prev => [...prev, newEmp]);
- setNewEmployee({ name: '', email: '', phone: '' });
- setShowAddDrawer(false);
- // Show success toast (simulated)
- console.log('Employee added successfully');
- }
- };
-
- const handleEditEmployee = (employee: Employee) => {
- setEditingEmployee(employee);
- setShowEditDrawer(true);
- };
-
- return (
-
- {/* Toolbar */}
-
-
-
-
-
-
- debouncedSearch(e.target.value)}
- aria-label="Search learners by name or email"
- />
-
-
-
-
-
-
- All Status
- Active
- Inactive
- Pending
-
-
-
-
-
setShowAddDrawer(true)}
- className="min-tap-44"
- aria-label="Add new learner"
- >
-
- Add Learner
-
-
setShowImportModal(true)}
- className="min-tap-44"
- aria-label="Import learners from CSV"
- >
-
- Import Learners
-
-
-
-
-
-
- {/* Bulk Action Bar */}
- {bulkActionVisible && (
-
-
-
-
- {selectedEmployees.length} learner{selectedEmployees.length !== 1 ? 's' : ''} selected {paginatedEmployees.length > 0 && selectedEmployees.length === paginatedEmployees.length ? '(current page)' : ''}
-
-
- setShowAssignModal(true)}
- className="min-tap-44"
- >
- Assign to Programme/Course
-
-
- Deactivate
-
-
- Reactivate
-
-
-
-
-
- )}
-
- {/* Learners Table */}
-
-
-
-
- Learners ({filteredEmployees.length})
-
- Manage learner accounts and assignments •
- Showing {startIndex + 1}-{Math.min(endIndex, filteredEmployees.length)} of {filteredEmployees.length} learners
-
-
-
- {
- setItemsPerPage(Number(value));
- setCurrentPage(1);
- setSelectedEmployees([]);
- }}>
-
-
-
-
- 5
- 10
- 20
- 50
-
-
- handleBulkSelect(selectedEmployees.length !== paginatedEmployees.length)}
- className="min-tap-44"
- >
- {selectedEmployees.length === paginatedEmployees.length && paginatedEmployees.length > 0 ? 'Deselect All' : 'Select All'}
-
-
-
-
-
-
-
-
-
- Select
- Name
- Email
- Phone
- Status
- Actions
-
-
-
- {paginatedEmployees.map((employee) => (
-
-
- handleEmployeeSelect(employee.id, !!checked)}
- className="min-tap-44"
- aria-label={`Select ${employee.name}`}
- />
-
- {employee.name}
- {employee.email}
- {employee.phone}
-
-
- {employee.status}
-
-
-
-
- handleEditEmployee(employee)}
- className="min-tap-44"
- aria-label={`Edit ${employee.name}`}
- >
-
-
-
-
-
-
-
-
- ))}
-
-
-
-
- {/* Pagination */}
- {totalPages > 1 && (
-
-
- Page {currentPage} of {totalPages}
-
-
-
-
- handlePageChange(Math.max(1, currentPage - 1))}
- className={currentPage === 1 ? 'pointer-events-none opacity-50' : 'cursor-pointer'}
- />
-
-
- {/* Show page numbers */}
- {(() => {
- const pages = [];
- const maxVisiblePages = 5;
- let startPage = Math.max(1, currentPage - Math.floor(maxVisiblePages / 2));
- let endPage = Math.min(totalPages, startPage + maxVisiblePages - 1);
-
- // Adjust startPage if we're near the end
- if (endPage - startPage + 1 < maxVisiblePages) {
- startPage = Math.max(1, endPage - maxVisiblePages + 1);
- }
-
- // Show first page if not in range
- if (startPage > 1) {
- pages.push(
-
- handlePageChange(1)}
- isActive={currentPage === 1}
- className="cursor-pointer"
- >
- 1
-
-
- );
- if (startPage > 2) {
- pages.push(
-
-
-
- );
- }
- }
-
- // Show page range
- for (let i = startPage; i <= endPage; i++) {
- pages.push(
-
- handlePageChange(i)}
- isActive={currentPage === i}
- className="cursor-pointer"
- >
- {i}
-
-
- );
- }
-
- // Show last page if not in range
- if (endPage < totalPages) {
- if (endPage < totalPages - 1) {
- pages.push(
-
-
-
- );
- }
- pages.push(
-
- handlePageChange(totalPages)}
- isActive={currentPage === totalPages}
- className="cursor-pointer"
- >
- {totalPages}
-
-
- );
- }
-
- return pages;
- })()}
-
-
- handlePageChange(Math.min(totalPages, currentPage + 1))}
- className={currentPage === totalPages ? 'pointer-events-none opacity-50' : 'cursor-pointer'}
- />
-
-
-
-
- )}
-
-
-
- {/* Add Learner Drawer */}
-
-
-
- Add New Learner
-
- Add a new learner to the system. Email cannot be changed after saving.
-
-
-
-
-
- Employee Name *
-
- setNewEmployee(prev => ({ ...prev, name: e.target.value }))}
- placeholder="Enter full name"
- required
- aria-required="true"
- />
-
-
-
- Email Address *
-
- setNewEmployee(prev => ({ ...prev, email: e.target.value }))}
- placeholder="email@company.com"
- required
- aria-required="true"
- />
-
-
-
- Phone Number
-
- setNewEmployee(prev => ({ ...prev, phone: e.target.value }))}
- placeholder="+61 4XX XXX XXX"
- />
-
-
-
- Save Learner
-
- setShowAddDrawer(false)} className="flex-1">
- Cancel
-
-
-
-
-
-
- {/* Import Modal */}
-
-
-
- Import Learners
-
- Upload a CSV file to import multiple learners. Maximum file size: 5MB.
-
-
-
-
-
Step 1: Download Template
-
- Download our CSV template with the required fields: Name, Email, Phone.
-
-
-
- Download CSV Template
-
-
-
-
Step 2: Upload File
-
-
-
- Drag and drop your CSV file here, or click to browse
-
-
- Choose File
-
-
-
-
- Import Learners
- setShowImportModal(false)} className="flex-1">
- Cancel
-
-
-
-
-
-
- {/* Assign Modal */}
-
-
-
- Assign to Programme/Course
-
- Assign {selectedEmployees.length} selected learner{selectedEmployees.length !== 1 ? 's' : ''} to a programme or course.
-
-
-
-
-
- Select Programme/Course
-
-
-
-
-
-
- Leadership Development
- Technical Skills
- Communication
- Project Management
-
-
-
-
-
- Start Date (Optional)
-
-
-
-
- Assign
- setShowAssignModal(false)} className="flex-1">
- Cancel
-
-
-
-
-
-
- {/* Edit/Assign Drawer */}
-
-
-
-
- {editingEmployee?.name}
-
-
- Edit learner details and manage course assignments.
-
-
- {editingEmployee && (
-
-
- Details
- Enrolments
-
-
-
-
- Name
-
-
-
-
-
- Phone
-
-
-
-
-
- Status
-
-
-
-
-
-
- Active
- Inactive
- Pending
-
-
-
-
-
-
-
-
Current Enrolments
-
-
- Assign Course
-
-
- {editingEmployee.programme && (
-
-
-
-
-
{editingEmployee.programme}
-
{editingEmployee.course}
- {editingEmployee.progress !== undefined && (
-
- )}
-
-
- Unassign
-
-
-
-
- )}
-
-
-
- Save Changes
- setShowEditDrawer(false)} className="flex-1">
- Cancel
-
-
-
- )}
-
-
-
- );
-};
-
-
-
-const TestimonialsTabContent: React.FC = () => {
- const [loading, setLoading] = useState(true);
- const [formData, setFormData] = useState({
- name: 'Alex Sharma',
- email: 'alex.sharma@company.com',
- phone: '',
- organisation: 'Acme Corp',
- programme: '',
- testimonialText: '',
- consentToPublish: false
- });
- const [formErrors, setFormErrors] = useState>({});
- const [isSubmitting, setIsSubmitting] = useState(false);
- const [submitSuccess, setSubmitSuccess] = useState(false);
- const [submitError, setSubmitError] = useState('');
- const [charCount, setCharCount] = useState(0);
- const [prefersReducedMotion] = useLocalStorage('prefersReducedMotion', false);
-
- // Simulate profile pre-fill loading
- useEffect(() => {
- const timer = setTimeout(() => setLoading(false), 300);
- return () => clearTimeout(timer);
- }, []);
-
- const programmes = [
- 'Leadership Development',
- 'Technical Skills',
- 'Communication',
- 'Project Management',
- 'Sales Training'
- ];
-
- const validateForm = () => {
- const errors: Record = {};
-
- if (!formData.testimonialText.trim()) {
- errors.testimonialText = 'Testimonial text is required';
- } else if (formData.testimonialText.length < 1) {
- errors.testimonialText = 'Testimonial must be at least 1 character';
- } else if (formData.testimonialText.length > 2000) {
- errors.testimonialText = 'Testimonial must be 2000 characters or less';
- }
-
- if (!formData.consentToPublish) {
- errors.consentToPublish = 'You must consent to publish your testimonial';
- }
-
- return errors;
- };
-
- const isFormValid = () => {
- const errors = validateForm();
- return Object.keys(errors).length === 0 && formData.testimonialText.trim().length > 0;
- };
-
- const handleInputChange = (field: keyof TestimonialFormData, value: string | boolean) => {
- setFormData(prev => ({ ...prev, [field]: value }));
-
- if (field === 'testimonialText' && typeof value === 'string') {
- setCharCount(value.length);
- }
-
- // Clear field error when user starts typing
- if (formErrors[field]) {
- setFormErrors(prev => {
- const newErrors = { ...prev };
- delete newErrors[field];
- return newErrors;
- });
- }
- };
-
- const handleSubmit = async (e: React.FormEvent) => {
- e.preventDefault();
-
- const errors = validateForm();
- if (Object.keys(errors).length > 0) {
- setFormErrors(errors);
- return;
- }
-
- setIsSubmitting(true);
- setSubmitError('');
-
- try {
- // Simulate API call
- await new Promise(resolve => setTimeout(resolve, 1500));
-
- // Simulate occasional error for testing
- if (Math.random() > 0.9) {
- throw new Error('Submission failed. Please try again.');
- }
-
- setSubmitSuccess(true);
- setFormData(prev => ({ ...prev, testimonialText: '', programme: '', consentToPublish: false }));
- setCharCount(0);
- } catch (error) {
- setSubmitError(error instanceof Error ? error.message : 'An error occurred. Please try again.');
- } finally {
- setIsSubmitting(false);
- }
- };
-
- const resetForm = () => {
- setSubmitSuccess(false);
- setSubmitError('');
- setFormErrors({});
- };
-
- if (loading) {
- return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
- }
-
- return (
-
- {/* Success State */}
- {submitSuccess && (
-
-
-
- Thanks — your testimonial is pending review by KLC.
-
- Submit another testimonial
-
-
-
- )}
-
- {/* Error State */}
- {submitError && (
-
-
-
- {submitError}
-
-
- )}
-
- {/* Testimonial Form */}
-
-
- Submit Testimonial
-
- Share your experience with KLC programmes to help others discover the value of our learning solutions.
-
-
-
-
-
-
-
- {/* Status Notice */}
-
-
-
-
-
-
- Submissions are reviewed by KLC Super-Admins before appearing publicly.
-
- View policy
-
-
-
-
-
-
-
-
- );
-};
-
-const DiscussionForumsScreen: React.FC = () => {
- const [selectedCohort, setSelectedCohort] = useState('1');
- const [viewMode, setViewMode] = useState<'index' | 'thread' | 'create'>('index');
- const [selectedThread, setSelectedThread] = useState(null);
- const [searchTerm, setSearchTerm] = useState('');
- const [filterTag, setFilterTag] = useState('all');
- const [showCreateThread, setShowCreateThread] = useState(false);
- const [newThread, setNewThread] = useState({ title: '', content: '', tags: '' });
- const [posts, setPosts] = useState(mockPosts);
- const [newPost, setNewPost] = useState('');
- const [currentUserId] = useState('user1'); // Current user ID - would come from auth
- const [prefersReducedMotion] = useLocalStorage('prefersReducedMotion', false);
-
- // Filter threads by selected cohort and search term
- const filteredThreads = mockThreads.filter(thread => {
- const matchesCohort = thread.cohortId === selectedCohort;
- const matchesSearch = thread.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
- thread.content.toLowerCase().includes(searchTerm.toLowerCase());
- const matchesTag = filterTag === 'all' || thread.tags?.includes(filterTag);
- return matchesCohort && matchesSearch && matchesTag;
- });
-
- // Get unique tags for filter
- const allTags = Array.from(new Set(mockThreads.flatMap(thread => thread.tags || [])));
-
- // Format relative time
- const formatRelativeTime = (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();
- };
-
- const handleOpenThread = (thread: Thread) => {
- setSelectedThread(thread);
- setViewMode('thread');
- };
-
- const handleBackToIndex = () => {
- setViewMode('index');
- setSelectedThread(null);
- setNewPost('');
- };
-
- const handleCreateThread = () => {
- if (newThread.title.trim() && newThread.content.trim()) {
- // In real app, this would make an API call
- console.log('Creating thread:', newThread);
- setNewThread({ title: '', content: '', tags: '' });
- setShowCreateThread(false);
- setViewMode('index');
- }
- };
-
- const handleAddPost = () => {
- if (newPost.trim() && selectedThread) {
- const post: Post = {
- id: Date.now().toString(),
- threadId: selectedThread.id,
- content: newPost,
- author: { id: currentUserId, name: 'You' },
- createdAt: new Date().toISOString(),
- reactions: {}
- };
- setPosts(prev => [...prev, post]);
- setNewPost('');
- }
- };
-
- const handleReaction = (postId: string, emoji: string) => {
- setPosts(prev => prev.map(post => {
- if (post.id === postId) {
- const reactions = { ...post.reactions };
- if (reactions[emoji]?.includes(currentUserId)) {
- reactions[emoji] = reactions[emoji].filter(id => id !== currentUserId);
- if (reactions[emoji].length === 0) delete reactions[emoji];
- } else {
- reactions[emoji] = [...(reactions[emoji] || []), currentUserId];
- }
- return { ...post, reactions };
- }
- return post;
- }));
- };
-
- const handleReportPost = (postId: string) => {
- console.log('Reporting post:', postId);
- // In real app, this would send to moderators
- };
-
- const selectedCohortData = mockCohorts.find(c => c.id === selectedCohort);
- const threadPosts = posts.filter(post => post.threadId === selectedThread?.id);
-
- // Create Thread Modal
- const CreateThreadModal = () => (
-
-
-
- Create New Thread
-
- Start a new discussion in {selectedCohortData?.name}
-
-
-
-
-
- Title *
-
-
setNewThread(prev => ({ ...prev, title: e.target.value }))}
- placeholder="What would you like to discuss?"
- maxLength={120}
- className="min-tap-44"
- />
-
- {newThread.title.length}/120 characters
-
-
-
-
- Content *
-
- setNewThread(prev => ({ ...prev, content: e.target.value }))}
- placeholder="Share your thoughts, ask questions, or start a conversation..."
- className="min-h-[120px] min-tap-44"
- />
-
-
-
- Tags (Optional)
-
- setNewThread(prev => ({ ...prev, tags: e.target.value }))}
- placeholder="leadership, teamwork, communication (separated by commas)"
- className="min-tap-44"
- />
-
-
-
- Create Thread
-
- setShowCreateThread(false)} className="flex-1">
- Cancel
-
-
-
-
-
- );
-
- // Thread Index View
- if (viewMode === 'index') {
- return (
-
- {/* Header */}
-
-
-
Discussion Forums
-
Connect, share, and learn with your cohort members
-
-
setShowCreateThread(true)}
- className="min-tap-44"
- >
-
- New Thread
-
-
-
- {/* Cohort Selector & Filters */}
-
-
-
-
-
-
-
-
-
-
-
- {mockCohorts.filter(cohort => cohort.isActive).map(cohort => (
-
-
- {cohort.name}
-
- {cohort.memberCount} members
-
-
-
- ))}
-
-
-
-
-
- setSearchTerm(e.target.value)}
- className="pl-10"
- />
-
-
-
-
-
-
-
- All Tags
- {allTags.map(tag => (
-
- #{tag}
-
- ))}
-
-
-
-
-
-
-
- {/* Threads List */}
-
-
-
-
-
- Discussions ({filteredThreads.length})
-
-
- {selectedCohortData?.name} • {selectedCohortData?.memberCount} members
-
-
-
-
-
-
- {filteredThreads.length === 0 ? (
-
-
-
- {searchTerm || filterTag !== 'all'
- ? 'No threads match your search criteria'
- : 'No discussions yet. Start the first conversation!'}
-
- {!searchTerm && filterTag === 'all' && (
-
setShowCreateThread(true)}
- className="mt-4"
- >
- Create First Thread
-
- )}
-
- ) : (
- filteredThreads.map(thread => (
-
handleOpenThread(thread)}
- role="button"
- tabIndex={0}
- onKeyDown={(e) => {
- if (e.key === 'Enter' || e.key === ' ') {
- e.preventDefault();
- handleOpenThread(thread);
- }
- }}
- >
-
-
-
- {thread.isPinned && (
-
- )}
-
{thread.title}
-
-
- {thread.content}
-
-
- By {thread.author.name}
- •
- {formatRelativeTime(thread.createdAt)}
- {thread.lastActivity !== thread.createdAt && (
- <>
- •
- Last activity {formatRelativeTime(thread.lastActivity)}
- >
- )}
-
- {thread.tags && thread.tags.length > 0 && (
-
- {thread.tags.slice(0, 3).map(tag => (
-
- #{tag}
-
- ))}
- {thread.tags.length > 3 && (
-
- +{thread.tags.length - 3}
-
- )}
-
- )}
-
-
-
-
-
- {thread.replyCount}
-
- {thread.reactions && Object.keys(thread.reactions).length > 0 && (
-
- {Object.entries(thread.reactions).slice(0, 2).map(([emoji, users]) => (
-
- {emoji}{users.length}
-
- ))}
-
- )}
-
-
-
-
- ))
- )}
-
-
-
-
-
-
- );
- }
-
- // Thread Detail View
- if (viewMode === 'thread' && selectedThread) {
- return (
-
- {/* Thread Header */}
-
-
-
- Back to Forums
-
-
- {selectedCohortData?.name}
-
- Thread
-
-
-
- {/* Thread Content */}
-
-
-
-
-
-
- {selectedThread.isPinned && (
-
- )}
-
{selectedThread.title}
-
-
- By {selectedThread.author.name}
- •
- {formatRelativeTime(selectedThread.createdAt)}
- •
- {selectedThread.replyCount} replies
-
-
-
-
-
-
-
-
-
- handleReportPost(selectedThread.id)}>
-
- Report Thread
-
-
-
- Share Thread
-
-
-
-
-
-
-
{selectedThread.content}
-
-
- {selectedThread.tags && selectedThread.tags.length > 0 && (
-
- {selectedThread.tags.map(tag => (
-
- #{tag}
-
- ))}
-
- )}
-
- {/* Thread Reactions */}
- {selectedThread.reactions && Object.keys(selectedThread.reactions).length > 0 && (
-
- {Object.entries(selectedThread.reactions).map(([emoji, users]) => (
- handleReaction(selectedThread.id, emoji)}
- >
- {emoji} {users.length}
-
- ))}
-
-
-
-
- )}
-
-
-
-
- {/* Posts */}
-
-
- Replies ({threadPosts.length})
-
-
-
- {threadPosts.map(post => (
-
-
-
-
-
- {post.author.name.split(' ').map(n => n[0]).join('')}
-
-
-
- {post.author.name}
-
- {formatRelativeTime(post.createdAt)}
- {post.editedAt && ' (edited)'}
-
-
-
-
-
-
-
-
-
-
-
-
- Reply
-
- {post.author.id === currentUserId && (
- <>
-
-
- Edit
-
-
-
- Delete
-
- >
- )}
-
- handleReportPost(post.id)}>
-
- Report
-
-
-
-
-
-
-
- {/* Post Reactions */}
- {post.reactions && Object.keys(post.reactions).length > 0 && (
-
- {Object.entries(post.reactions).map(([emoji, users]) => (
- handleReaction(post.id, emoji)}
- >
- {emoji} {users.length}
-
- ))}
-
-
-
-
- )}
-
- ))}
-
-
-
-
- {/* Reply Editor */}
-
-
- Add Reply
-
-
-
-
setNewPost(e.target.value)}
- placeholder="Share your thoughts or ask a follow-up question..."
- className="min-h-[100px] min-tap-44"
- />
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Post Reply
-
-
-
-
-
-
- );
- }
-
- return null;
-};
-
-const ReportsScreen: React.FC = () => {
- const [activeTab, setActiveTab] = useState('tracker');
- const [dateRange, setDateRange] = useState('last-30-days');
- const [selectedProgramme, setSelectedProgramme] = useState('all');
- const [selectedCohort, setSelectedCohort] = useState('all');
- const [exportingFormat, setExportingFormat] = useState(null);
- const [loading, setLoading] = useState(false);
- const [showExportOptions, setShowExportOptions] = useState(false);
-
- const programmes = [
- { id: 'all', name: 'All Programmes', learners: 247 },
- { id: 'leadership', name: 'Leadership Development', learners: 85 },
- { id: 'technical', name: 'Technical Skills', learners: 67 },
- { id: 'communication', name: 'Communication', learners: 45 },
- { id: 'project', name: 'Project Management', learners: 50 }
- ];
-
- const handleExport = async (format: 'excel' | 'pdf', reportType: string = 'comprehensive') => {
- setExportingFormat(format);
-
- // Simulate export process
- await new Promise(resolve => setTimeout(resolve, 2500));
-
- const programmeName = programmes.find(p => p.id === selectedProgramme)?.name || 'All Programmes';
- const fileName = `${programmeName.replace(/\s+/g, '_')}_${reportType}_Report_${dateRange}.${format === 'excel' ? 'xlsx' : 'pdf'}`;
-
- console.log(`Exported ${fileName} as ${format.toUpperCase()}`);
-
- // Show success message
- alert(`✅ Successfully exported ${fileName}`);
-
- setExportingFormat(null);
- setShowExportOptions(false);
- };
-
- const generateReport = () => {
- setLoading(true);
- setTimeout(() => setLoading(false), 1500);
- };
-
- // Mock data for reports
- const trackerMetrics = [
- { metric: 'Completion Rate', current: 78, previous: 72, trend: 'up' },
- { metric: 'Average Score', current: 85, previous: 82, trend: 'up' },
- { metric: 'Time to Complete', current: 21, previous: 24, trend: 'up' },
- { metric: 'Engagement Rate', current: 92, previous: 89, trend: 'up' }
- ];
-
- const individualReports = [
- { id: '1', name: 'Sarah Chen', programme: 'Leadership Development', progress: 85, lastAccess: '2 hours ago', status: 'On Track' },
- { id: '2', name: 'Michael Rodriguez', programme: 'Technical Skills', progress: 62, lastAccess: '1 day ago', status: 'Behind' },
- { id: '3', name: 'Emma Thompson', programme: 'Communication', progress: 94, lastAccess: '3 hours ago', status: 'Completed' },
- { id: '4', name: 'David Kim', programme: 'Project Management', progress: 78, lastAccess: '5 hours ago', status: 'On Track' },
- { id: '5', name: 'Lisa Wang', programme: 'Leadership Development', progress: 56, lastAccess: '2 days ago', status: 'Behind' }
- ];
-
- const cohortData = [
- { cohort: 'Leadership Q4 2024', enrolled: 45, completed: 32, inProgress: 11, notStarted: 2, avgScore: 87 },
- { cohort: 'Technical Skills Cohort A', enrolled: 38, completed: 25, inProgress: 10, notStarted: 3, avgScore: 82 },
- { cohort: 'Communication Workshop', enrolled: 28, completed: 28, inProgress: 0, notStarted: 0, avgScore: 91 },
- { cohort: 'Project Management Cert', enrolled: 52, completed: 18, inProgress: 28, notStarted: 6, avgScore: 79 }
- ];
-
- return (
-
- {/* Reports Header */}
-
-
-
HR Access Reports
-
Programme-wise reporting and analytics with Excel/PDF export
-
-
-
-
-
-
-
- {programmes.map(programme => (
-
-
- {programme.name}
-
- {programme.learners} learners
-
-
-
- ))}
-
-
-
-
-
-
-
- Last 7 days
- Last 30 days
- Last 90 days
- Last year
-
-
-
-
-
- {exportingFormat ? (
- <>
-
- Exporting {exportingFormat.toUpperCase()}...
- >
- ) : (
- <>
-
- Export Reports
-
- >
- )}
-
-
-
-
-
Export Options
-
- {programmes.find(p => p.id === selectedProgramme)?.name} • {dateRange}
-
-
-
- handleExport('excel', 'comprehensive')}
- className="flex items-center justify-between"
- >
-
-
-
-
Excel Report
-
Detailed data with charts
-
-
- XLSX
-
- handleExport('pdf', 'comprehensive')}
- className="flex items-center justify-between"
- >
-
-
-
-
PDF Report
-
Formatted executive summary
-
-
- PDF
-
-
- handleExport('excel', 'tracker')}
- className="flex items-center justify-between"
- >
-
-
-
-
Tracker Data
-
Progress tracking spreadsheet
-
-
- XLSX
-
- handleExport('excel', 'learner-list')}
- className="flex items-center justify-between"
- >
-
-
-
-
Learner List
-
Contact details & progress
-
-
- XLSX
-
-
-
-
-
-
- {/* Programme Overview Card */}
- {selectedProgramme !== 'all' && (
-
-
-
-
-
- {programmes.find(p => p.id === selectedProgramme)?.name} - Report Overview
-
-
- Data summary for {dateRange} • Ready for export
-
-
-
- {programmes.find(p => p.id === selectedProgramme)?.learners} Active Learners
-
-
-
-
-
-
- {Math.floor(Math.random() * 30 + 60)}%
-
-
Completion Rate
-
-
-
- {Math.floor(Math.random() * 10 + 80)}/100
-
-
Average Score
-
-
-
- {Math.floor(Math.random() * 10 + 15)} days
-
-
Avg. Time to Complete
-
-
-
- {Math.floor(Math.random() * 20 + 80)}%
-
-
Engagement Rate
-
-
-
-
-
-
- All data validated and available for export. Choose Excel for detailed analysis or PDF for executive summary.
-
-
-
-
- )}
-
- {/* Reports Tabs */}
-
-
-
-
-
-
- Tracker
-
-
-
- Individual Reports
-
-
-
- Cohort Reports
-
-
-
- {/* Tracker Tab */}
-
-
-
-
-
- Learning Progress Tracker
-
- Real-time metrics for {programmes.find(p => p.id === selectedProgramme)?.name || 'All Programmes'}
-
-
-
- handleExport('excel', 'tracker')}
- disabled={!!exportingFormat}
- className="min-tap-44"
- >
- {exportingFormat === 'excel' ? (
-
- ) : (
-
- )}
- Export Tracker Excel
-
- handleExport('pdf', 'tracker')}
- disabled={!!exportingFormat}
- className="min-tap-44"
- >
- {exportingFormat === 'pdf' ? (
-
- ) : (
-
- )}
- Export PDF
-
-
-
-
-
- {/* Tracker KPIs */}
-
- {trackerMetrics.map((metric, index) => (
-
-
-
-
-
{metric.metric}
-
- {metric.current}
- {metric.metric.includes('Rate') ? '%' : metric.metric.includes('Time') ? ' days' : metric.metric.includes('Score') ? '/100' : ''}
-
-
-
- {metric.trend === 'up' ? '+' : ''}
- {metric.current - metric.previous}
- {metric.metric.includes('Rate') ? '%' : ''}
-
-
-
-
- ))}
-
-
- {/* Progress Charts */}
-
-
-
- Completion Trends
- Weekly completion rates over the last 12 weeks
-
-
-
-
-
-
-
-
-
-
-
-
-
- `${value}%`}
- />
- {
- if (active && payload && payload.length) {
- return (
-
-
{label}
-
-
- ● Completion Rate: {payload[0].value}%
-
-
- Completed: {payload[0].payload.completed} / Started: {payload[0].payload.started}
-
-
-
- );
- }
- return null;
- }}
- />
-
-
-
-
-
- Area chart showing completion rate trends over 12 weeks, ranging from 48% to 94%
-
-
-
-
- Average: {Math.round(completionTrendsData.reduce((acc, curr) => acc + curr.completionRate, 0) / completionTrendsData.length)}%
-
-
-
-
-
-
-
- Activity Heatmap
- Learning activity patterns by day and time
-
-
-
-
- {activityHeatmapData.map((dayData, dayIndex) => (
-
-
- {dayData.day}
-
-
- {(['6AM', '9AM', '12PM', '3PM', '6PM', '9PM'] as const).map((timeSlot) => {
- const value = dayData[timeSlot];
- const intensity = Math.min(value / 45, 1); // Max activity is around 45
- return (
-
{
- if (e.key === 'Enter' || e.key === ' ') {
- e.preventDefault();
- console.log(`${dayData.day} ${timeSlot}: ${value} activities`);
- }
- }}
- />
- );
- })}
-
-
- ))}
-
-
-
- Heatmap showing learning activity intensity across days of the week and time periods
-
-
- {/* Heatmap Legend */}
-
-
-
-
Activity Level:
-
-
Low
-
- {[0.2, 0.4, 0.6, 0.8, 1.0].map((intensity, index) => (
-
- ))}
-
-
High
-
-
-
- Peak: Wed 3PM (42 activities)
-
-
-
- {/* Time Labels */}
-
-
Time slots:
-
- {['6AM', '9AM', '12PM', '3PM', '6PM', '9PM'].map((time) => (
- {time}
- ))}
-
-
-
-
-
-
-
-
-
-
- {/* Individual Reports Tab */}
-
-
-
-
-
- Individual Learner Reports
-
- Detailed progress reports for {programmes.find(p => p.id === selectedProgramme)?.name || 'All Programmes'}
-
-
-
- handleExport('excel', 'learner-list')}
- disabled={!!exportingFormat || loading}
- className="min-tap-44"
- >
- {exportingFormat === 'excel' ? (
-
- ) : (
-
- )}
- Export Excel
-
- handleExport('pdf', 'individual')}
- disabled={!!exportingFormat || loading}
- className="min-tap-44"
- >
- {exportingFormat === 'pdf' ? (
-
- ) : (
-
- )}
- Export PDF
-
-
- {loading ? (
-
- ) : (
-
- )}
- Refresh Data
-
-
-
-
-
-
-
-
-
- Learner
- Programme
- Progress
- Last Access
- Status
- Actions
-
-
-
- {individualReports.map((learner) => (
-
- {learner.name}
- {learner.programme}
-
-
-
-
{learner.progress}%
-
-
- {learner.lastAccess}
-
-
- {learner.status}
-
-
-
-
-
-
-
-
- ))}
-
-
-
-
-
-
-
- {/* Cohort Reports Tab */}
-
-
-
-
-
- Cohort Performance Reports
-
- Group-level analytics for {programmes.find(p => p.id === selectedProgramme)?.name || 'All Programmes'}
-
-
-
-
-
-
-
-
- All Cohorts
- Leadership Q4 2024
- Technical Skills Cohort A
- Communication Workshop
- Project Management Cert
-
-
- handleExport('excel', 'cohort')}
- disabled={!!exportingFormat}
- className="min-tap-44"
- >
- {exportingFormat === 'excel' ? (
-
- ) : (
-
- )}
- Export Excel
-
- handleExport('pdf', 'cohort')}
- disabled={!!exportingFormat}
- className="min-tap-44"
- >
- {exportingFormat === 'pdf' ? (
-
- ) : (
-
- )}
- Export PDF
-
-
-
-
-
-
- {/* Cohort Summary Cards */}
- {cohortData.map((cohort, index) => (
-
-
-
-
-
{cohort.cohort}
-
{cohort.enrolled} total learners enrolled
-
-
-
-
-
{cohort.completed}
-
Completed
-
-
-
{cohort.inProgress}
-
In Progress
-
-
-
{cohort.notStarted}
-
Not Started
-
-
-
{cohort.avgScore}/100
-
Avg Score
-
-
-
-
- {/* Progress Bar */}
-
-
- Completion Progress
- {Math.round((cohort.completed / cohort.enrolled) * 100)}%
-
-
-
-
-
- ))}
-
-
-
-
-
-
-
-
- {/* Export Information Card */}
-
-
-
-
- Export Information
-
-
- Understanding what's included in each report format
-
-
-
-
- {/* Excel Reports */}
-
-
-
-
Excel Reports (.xlsx)
-
-
-
-
-
-
Comprehensive Data
-
Raw data, pivot tables, and interactive charts
-
-
-
-
-
-
Multiple Worksheets
-
Learner details, progress tracking, and analytics
-
-
-
-
-
-
Filterable & Sortable
-
Custom analysis and deeper insights
-
-
-
-
-
- {/* PDF Reports */}
-
-
-
-
PDF Reports (.pdf)
-
-
-
-
-
-
Executive Summary
-
Key metrics and performance highlights
-
-
-
-
-
-
Visual Charts
-
Graphs and progress visualizations
-
-
-
-
-
-
Share-Ready
-
Professional format for stakeholders
-
-
-
-
-
-
-
-
-
-
-
Automated Data Updates
-
- All reports include real-time data up to the moment of export.
- Export time and date range are included in file metadata.
-
-
-
-
-
-
-
- );
-};
-
-const ProfileScreen: React.FC = () => {
- const [activeTab, setActiveTab] = useState('organisation');
-
- return (
-
- {/* Profile Tabs */}
-
-
-
-
-
-
- Organisation
- Org
-
-
-
- Billing
-
-
-
- Roles
-
-
-
- Testimonials
- Reviews
-
-
-
-
-
-
- Organisation Profile
- Manage your organisation's profile and settings
-
-
-
-
-
Organisation profile settings would be displayed here
-
-
-
-
-
-
-
-
- Billing & Subscriptions
- Manage your billing information and subscription plans
-
-
-
-
-
Billing and subscription management would be displayed here
-
-
-
-
-
-
-
-
- Roles & Permissions
- Manage user roles and access permissions
-
-
-
-
-
Roles and permissions management would be displayed here
-
-
-
-
-
-
-
-
-
-
-
-
- );
-};
-
-// Main App Component
-export default function App() {
- const [currentScreen, setCurrentScreen] = useState('home');
- const [isDark, setIsDark] = useLocalStorage('darkMode', false);
- const [prefersReducedMotion, setPrefersReducedMotion] = useLocalStorage('prefersReducedMotion', false);
- const [sidebarOpen, setSidebarOpen] = useState(false);
- const [announcementsOpen, setAnnouncementsOpen] = useState(false);
- const [screenFilters, setScreenFilters] = useState({});
- const [currentProgrammeId, setCurrentProgrammeId] = useState(null);
- const [currentCourseId, setCurrentCourseId] = useState(null);
-
- // Apply theme
- useEffect(() => {
- document.documentElement.classList.toggle('dark', isDark);
- }, [isDark]);
-
- // Handle navigation
- const handleNavigate = useCallback((screen: string, filters?: any) => {
- setCurrentScreen(screen);
-
- if (filters) {
- setScreenFilters(prev => ({ ...prev, [screen]: filters }));
- }
- setSidebarOpen(false); // Close sidebar on mobile after navigation
- }, []);
-
- const handleNotificationToggle = () => {
- setAnnouncementsOpen(!announcementsOpen);
- };
-
- const handleMarkAsRead = (id: string) => {
- console.log(`Marked notification ${id} as read`);
- };
-
- // HR View handlers
- const handleViewProgramme = (programmeId: string) => {
- setCurrentProgrammeId(programmeId);
- setCurrentScreen('programme-view');
- };
-
- const handleViewCourse = (courseId: string) => {
- setCurrentCourseId(courseId);
- setCurrentScreen('course-view');
- };
-
- const handleBackToHome = () => {
- setCurrentScreen('home');
- setCurrentProgrammeId(null);
- setCurrentCourseId(null);
- };
-
- const handleAssignLearners = (id: string) => {
- console.log(`Opening assignment wizard for ${id}`);
- // This would open the assignment wizard component
- };
-
- const handleDownloadTracker = (id: string) => {
- console.log(`Downloading tracker for ${id}`);
- // This would trigger the Excel download
- };
-
- const handleOpenAnalytics = (id: string) => {
- console.log(`Opening analytics for ${id}`);
- // This would navigate to analytics with filters
- setCurrentScreen('reports');
- setScreenFilters(prev => ({ ...prev, reports: { programmeId: id } }));
- };
-
- const renderCurrentScreen = () => {
- const filters = screenFilters[currentScreen];
-
- switch (currentScreen) {
- case 'home':
- return ;
- case 'learners':
- return ;
- case 'discussions':
- return ;
- case 'reports':
- return ;
- case 'profile':
- return ;
- case 'programme-view':
- return currentProgrammeId ? (
-
- ) : ;
- case 'course-view':
- return currentCourseId ? (
-
- ) : ;
- default:
- return ;
- }
- };
-
- return (
-
- {/* Skip to main content link for accessibility */}
-
- Skip to main content
-
-
- {/* Top Navigation */}
-
setSidebarOpen(!sidebarOpen)}
- showMenuButton={true}
- onNotificationToggle={handleNotificationToggle}
- notificationCount={mockAnnouncements.length}
- />
-
-
- {/* Desktop Sidebar */}
-
-
- {/* Mobile Sidebar Overlay */}
- {sidebarOpen && (
-
-
setSidebarOpen(false)}
- />
-
-
- )}
-
- {/* Main Content */}
-
-
-
- {renderCurrentScreen()}
-
-
-
-
- {/* Announcements Panel */}
-
setAnnouncementsOpen(false)}
- announcements={mockAnnouncements}
- onMarkAsRead={handleMarkAsRead}
- />
-
- {/* Chat Bot FAB */}
-
-
- {/* Footer */}
-
-
- );
-}
\ No newline at end of file
+export default App;
\ No newline at end of file
diff --git a/src/App_new.tsx b/src/App_new.tsx
index 24dbc92..e69de29 100644
--- a/src/App_new.tsx
+++ b/src/App_new.tsx
@@ -1,2343 +0,0 @@
-import React, { useState, useEffect, useCallback } from 'react';
-import { Button } from './components/ui/button';
-import { Card, CardContent, CardDescription, CardHeader, CardTitle } from './components/ui/card';
-import { Badge } from './components/ui/badge';
-import { Input } from './components/ui/input';
-import { Textarea } from './components/ui/textarea';
-import { Checkbox } from './components/ui/checkbox';
-import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './components/ui/select';
-import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from './components/ui/dialog';
-import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle, SheetTrigger } from './components/ui/sheet';
-import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from './components/ui/table';
-import { Progress } from './components/ui/progress';
-import { Skeleton } from './components/ui/skeleton';
-import { Tabs, TabsContent, TabsList, TabsTrigger } from './components/ui/tabs';
-import { Breadcrumb, BreadcrumbItem, BreadcrumbLink, BreadcrumbList, BreadcrumbPage, BreadcrumbSeparator } from './components/ui/breadcrumb';
-import { Alert, AlertDescription } from './components/ui/alert';
-import { ActiveProgrammesTable } from './components/ActiveProgrammesTable';
-import { ProgrammeCalendar } from './components/ProgrammeCalendar';
-import { LearningAnalyticsTable } from './components/LearningAnalyticsTable';
-import { DiscussionForumFeed } from './components/DiscussionForumFeed';
-import {
- Home,
- Users,
- BarChart3,
- MessageSquare,
- Settings,
- Search,
- Filter,
- Download,
- Plus,
- Upload,
- Edit,
- MoreHorizontal,
- TrendingUp,
- Calendar,
- Clock,
- BookOpen,
- Award,
- Bell,
- ChevronDown,
- Menu,
- X,
- ArrowLeft,
- ChevronRight,
- RefreshCw,
- CheckCircle,
- AlertCircle,
- XCircle,
- FileText,
- Mail,
- Phone,
- Eye,
- Trash2,
- Building2,
- CreditCard,
- Shield,
- ExternalLink,
- Info
-} from 'lucide-react';
-
-// Types
-interface KPIData {
- title: string;
- value: number;
- change?: number;
- trend?: 'up' | 'down' | 'neutral';
-}
-
-interface Employee {
- id: string;
- name: string;
- email: string;
- phone: string;
- status: 'Active' | 'Inactive' | 'Pending';
- programme?: string;
- course?: string;
- progress?: number;
- lastActivity?: string;
-}
-
-interface Announcement {
- id: string;
- title: string;
- content: string;
- type: 'announcement' | 'reminder';
- timestamp: string;
- pinned?: boolean;
-}
-
-interface Deadline {
- id: string;
- title: string;
- type: 'webinar' | 'profiler';
- dueDate: string;
- dueTime: string;
-}
-
-interface TestimonialFormData {
- name: string;
- email: string;
- phone: string;
- organisation: string;
- programme: string;
- testimonialText: string;
- consentToPublish: boolean;
-}
-
-// Custom hooks
-const useLocalStorage = (key: string, initialValue: any) => {
- const [storedValue, setStoredValue] = useState(() => {
- try {
- const item = window.localStorage.getItem(key);
- return item ? JSON.parse(item) : initialValue;
- } catch (error) {
- return initialValue;
- }
- });
-
- const setValue = (value: any) => {
- try {
- setStoredValue(value);
- window.localStorage.setItem(key, JSON.stringify(value));
- } catch (error) {
- console.error('Error saving to localStorage:', error);
- }
- };
-
- return [storedValue, setValue];
-};
-
-const 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;
-};
-
-// Mock data
-const mockKPIData: KPIData[] = [
- { title: 'Total Learners', value: 1247, change: 12, trend: 'up' },
- { title: 'Active Courses', value: 89, change: 5, trend: 'up' },
- { title: 'Completed Profilers', value: 342, change: -8, trend: 'down' },
- { title: 'Average Progress', value: 73, change: 7, trend: 'up' }
-];
-
-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' }
-];
-
-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' }
-];
-
-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' }
-];
-
-// Components
-const KPICard: React.FC<{ data: KPIData; onClick?: () => void; className?: string }> = ({ data, onClick, className = '' }) => {
- const countedValue = useCountUp(data.value);
- const [prefersReducedMotion] = useLocalStorage('prefersReducedMotion', false);
-
- return (
- {
- if (e.key === 'Enter' || e.key === ' ') {
- e.preventDefault();
- onClick?.();
- }
- }}
- >
-
- {data.title}
-
-
-
-
- {prefersReducedMotion ? data.value : countedValue}
- {data.title.includes('Progress') && '%'}
-
- {data.change && (
-
- {data.trend === 'up' ? '+' : ''}{data.change}{data.title.includes('Progress') ? '%' : ''}
-
- )}
-
-
-
- );
-};
-
-const EmployeeProgressCard: React.FC<{
- employee: Employee;
- onEdit?: (employee: Employee) => void;
-}> = ({ employee, onEdit }) => {
- const getProgressColor = (progress: number) => {
- if (progress >= 80) return 'bg-status-success';
- if (progress >= 60) return 'bg-status-warn';
- return 'bg-status-error';
- };
-
- const getProgressStatus = (progress: number) => {
- if (progress === 0) return 'Not Started';
- if (progress >= 80) return 'Excellent';
- if (progress >= 60) return 'Good';
- return 'Needs Attention';
- };
-
- return (
- onEdit?.(employee)}>
-
-
-
-
- {employee.name.split(' ').map(n => n[0]).join('')}
-
-
-
{employee.name}
-
- {employee.status}
-
-
-
-
{ e.stopPropagation(); onEdit?.(employee); }}>
-
-
-
-
- {employee.programme && (
-
-
-
-
{employee.programme}
-
{employee.course}
-
-
{employee.progress}%
-
-
-
-
-
-
- {getProgressStatus(employee.progress || 0)}
-
- {employee.lastActivity}
-
-
-
- )}
-
-
- );
-};
-
-const EmployeeTable: React.FC<{
- employees: Employee[];
- onEdit?: (employee: Employee) => void;
- showProgress?: boolean;
- maxHeight?: string;
- viewMode?: 'table' | 'cards';
-}> = ({ employees, onEdit, showProgress = true, maxHeight = "400px", viewMode = 'table' }) => {
- if (viewMode === 'cards') {
- return (
-
- {employees.map((employee) => (
-
- ))}
-
- );
- }
-
- return (
-
-
-
-
- Employee
- Email
- Phone
- Status
- {showProgress && (
- <>
- Programme/Course
- Progress
- Last Activity
- >
- )}
- Actions
-
-
-
- {employees.map((employee) => (
- onEdit?.(employee)}
- role="button"
- tabIndex={0}
- onKeyDown={(e) => {
- if (e.key === 'Enter' || e.key === ' ') {
- e.preventDefault();
- onEdit?.(employee);
- }
- }}
- >
- {employee.name}
- {employee.email}
- {employee.phone}
-
-
- {employee.status}
-
-
- Employee status is {employee.status}
-
-
- {showProgress && (
- <>
-
-
-
{employee.programme}
-
{employee.course}
-
-
-
- {employee.progress !== undefined && (
-
-
-
{employee.progress}%
-
- Progress: {employee.progress} percent complete
-
-
- )}
-
- {employee.lastActivity}
- >
- )}
-
- {
- e.stopPropagation();
- onEdit?.(employee);
- }}
- aria-label={`Edit ${employee.name}`}
- >
-
-
-
-
- ))}
-
-
-
- );
-};
-
-const HRSidebar: React.FC<{
- activeScreen: string;
- onNavigate: (screen: string) => void;
- className?: string;
-}> = ({ activeScreen, onNavigate, className = '' }) => {
- const [prefersReducedMotion] = useLocalStorage('prefersReducedMotion', false);
-
- const menuItems = [
- { id: 'home', label: 'Dashboard', icon: Home, path: '/hr/home' },
- { id: 'learners', label: 'Learners', icon: Users, path: '/hr/learners' },
- { id: 'reports', label: 'Reports', icon: BarChart3, path: '/hr/reports' },
- { id: 'profile', label: 'Profile', icon: Settings, path: '/hr/profile' }
- ];
-
- return (
-
-
-
-
-
- {menuItems.map((item) => {
- const Icon = item.icon;
- const isActive = activeScreen === item.id;
-
- return (
-
- onNavigate(item.id)}
- className={`
- 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-current={isActive ? 'page' : undefined}
- aria-label={`Navigate to ${item.label}`}
- >
-
- {item.label}
-
-
- );
- })}
-
-
-
- );
-};
-
-const TopNav: React.FC<{
- onMenuToggle?: () => void;
- showMenuButton?: boolean;
- onNotificationToggle?: () => void;
- notificationCount?: number;
-}> = ({ onMenuToggle, showMenuButton = false, onNotificationToggle, notificationCount = 0 }) => {
- return (
-
-
- {showMenuButton && (
-
-
-
- )}
-
-
- KLC
-
-
Knowledge Learning Centre
-
-
-
-
-
-
- {notificationCount > 0 && (
-
- {notificationCount > 9 ? '9+' : notificationCount}
-
- )}
-
-
- HR
-
-
-
- );
-};
-
-const BreadcrumbNav: React.FC<{ currentScreen: string }> = ({ currentScreen }) => {
- const getBreadcrumbText = (screen: string) => {
- switch (screen) {
- case 'home': return 'Dashboard';
- case 'learners': return 'Learners';
- case 'reports': return 'Reports';
- case 'profile': return 'Profile';
- default: return 'HR Portal';
- }
- };
-
- return (
-
-
-
- HR Portal
-
-
-
- {getBreadcrumbText(currentScreen)}
-
-
-
- );
-};
-
-const AnnouncementsPanel: React.FC<{
- isOpen: boolean;
- onClose: () => void;
- announcements: Announcement[];
- onMarkAsRead?: (id: string) => void;
-}> = ({ isOpen, onClose, announcements, onMarkAsRead }) => {
- if (!isOpen) return null;
-
- return (
-
-
-
Announcements & Reminders
-
-
-
-
-
-
- {announcements.map((item) => (
-
onMarkAsRead?.(item.id)}
- >
-
-
-
- {item.type === 'announcement' ?
- :
-
- }
-
- {item.pinned && (
-
Pinned
- )}
-
-
{item.timestamp}
-
-
-
{item.title}
-
{item.content}
-
-
-
- {item.type}
-
-
- Mark as read
-
-
-
- ))}
-
-
-
-
- View All Notifications
-
-
-
- );
-};
-
-const ChatBot: React.FC<{ currentScreen?: string }> = ({ 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 (
-
- {isOpen && (
-
-
-
HR Assistant
- setIsOpen(false)}
- className="h-6 w-6"
- aria-label="Close chat"
- >
-
-
-
-
- {chips.map((chip, index) => (
-
- {chip}
-
- ))}
-
-
- )}
-
setIsOpen(!isOpen)}
- className="rounded-full h-12 w-12 shadow-lg min-tap-44"
- aria-label="Open HR chat assistant"
- aria-expanded={isOpen}
- >
-
-
-
- );
-};
-
-// Screen Components
-const HRHomeScreen: React.FC<{ onNavigate: (screen: string, filters?: any) => void }> = ({ onNavigate }) => {
- const [loading, setLoading] = useState(true);
- const [prefersReducedMotion] = useLocalStorage('prefersReducedMotion', false);
-
- useEffect(() => {
- const timer = setTimeout(() => setLoading(false), 800);
- return () => clearTimeout(timer);
- }, []);
-
- if (loading) {
- return (
-
-
- {[...Array(4)].map((_, i) => (
-
-
-
-
-
-
-
-
- ))}
-
-
-
-
-
-
-
-
-
-
- );
- }
-
- return (
-
- {/* Welcome Section */}
-
-
-
Corporate HR Dashboard
-
Manage programmes, track progress, and stay connected with your learning community
-
-
-
- {/* Quick Actions Section - Unchanged */}
-
-
- Quick Actions
- Common HR tasks and shortcuts
-
-
-
- {[
- { title: 'Add Learners', icon: Plus, action: () => onNavigate('learners', { action: 'add' }) },
- { title: 'Assign', icon: BookOpen, action: () => onNavigate('learners', { action: 'assign' }) },
- { title: 'Download Reports', icon: Download, action: () => onNavigate('reports') },
- { title: 'Submit Testimonial', icon: MessageSquare, action: () => onNavigate('profile') }
- ].map((link, index) => {
- const Icon = link.icon;
- return (
-
-
- {link.title}
-
- );
- })}
-
-
-
-
- {/* Active Programmes Table */}
-
-
console.log(`View programme: ${programmeId}`)}
- onAssignLearners={(programmeId) => console.log(`Assign learners to: ${programmeId}`)}
- onDownloadTracker={(programmeId) => console.log(`Download tracker for: ${programmeId}`)}
- />
-
-
- {/* Two Column Layout: Calendar + Learning Analytics */}
-
- {/* Programme Calendar - Right Side */}
-
-
console.log(`Open event: ${event.title}`)}
- />
-
-
- {/* Learning Analytics - Left Side (Larger) */}
-
- onNavigate('learners', { editEmployee: learnerId })}
- onNudgeLearner={(learnerId) => console.log(`Nudge learner: ${learnerId}`)}
- onViewAllAnalytics={(programmeId) => onNavigate('reports', { programme: programmeId })}
- />
-
-
-
- {/* Discussion Forum Feed */}
-
- console.log(`Open thread: ${threadId}`)}
- onMarkAsRead={(threadId) => console.log(`Mark as read: ${threadId}`)}
- />
-
-
- );
-};
-
-const LearnersScreen: React.FC<{ filters?: any }> = ({ filters }) => {
- const [employees, setEmployees] = useState(mockEmployees);
- const [searchTerm, setSearchTerm] = useState('');
- const [statusFilter, setStatusFilter] = useState('all');
- const [selectedEmployees, setSelectedEmployees] = useState([]);
- const [showAddDrawer, setShowAddDrawer] = useState(false);
- const [showImportModal, setShowImportModal] = useState(false);
- const [showAssignModal, setShowAssignModal] = useState(false);
- const [showEditDrawer, setShowEditDrawer] = useState(false);
- const [editingEmployee, setEditingEmployee] = useState(null);
- const [newEmployee, setNewEmployee] = useState({ name: '', email: '', phone: '' });
- const [bulkActionVisible, setBulkActionVisible] = useState(false);
-
- const debouncedSearch = useCallback(
- (term: string) => {
- // Simulating search with 300ms delay
- 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;
- return matchesSearch && matchesStatus;
- });
-
- 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(filteredEmployees.map(emp => emp.id));
- } else {
- setSelectedEmployees([]);
- }
- };
-
- useEffect(() => {
- setBulkActionVisible(selectedEmployees.length > 0);
- }, [selectedEmployees]);
-
- 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'
- };
- setEmployees(prev => [...prev, newEmp]);
- setNewEmployee({ name: '', email: '', phone: '' });
- setShowAddDrawer(false);
- // Show success toast (simulated)
- console.log('Employee added successfully');
- }
- };
-
- const handleEditEmployee = (employee: Employee) => {
- setEditingEmployee(employee);
- setShowEditDrawer(true);
- };
-
- return (
-
- {/* Toolbar */}
-
-
-
-
-
-
- debouncedSearch(e.target.value)}
- aria-label="Search learners by name or email"
- />
-
-
-
-
-
-
- All Status
- Active
- Inactive
- Pending
-
-
-
-
-
setShowAddDrawer(true)}
- className="min-tap-44"
- aria-label="Add new learner"
- >
-
- Add Learner
-
-
setShowImportModal(true)}
- className="min-tap-44"
- aria-label="Import learners from CSV"
- >
-
- Import Learners
-
-
-
-
-
-
- {/* Bulk Action Bar */}
- {bulkActionVisible && (
-
-
-
-
- {selectedEmployees.length} learner{selectedEmployees.length !== 1 ? 's' : ''} selected
-
-
- setShowAssignModal(true)}
- className="min-tap-44"
- >
- Assign to Programme/Course
-
-
- Deactivate
-
-
- Reactivate
-
-
-
-
-
- )}
-
- {/* Learners Table */}
-
-
-
-
- Learners ({filteredEmployees.length})
- Manage learner accounts and assignments
-
-
- handleBulkSelect(selectedEmployees.length !== filteredEmployees.length)}
- className="min-tap-44"
- >
- {selectedEmployees.length === filteredEmployees.length ? 'Deselect All' : 'Select All'}
-
-
-
-
-
-
-
-
-
- {/* Add Learner Drawer */}
-
-
-
- Add New Learner
-
- Add a new learner to the system. Email cannot be changed after saving.
-
-
-
-
-
- Employee Name *
-
- setNewEmployee(prev => ({ ...prev, name: e.target.value }))}
- placeholder="Enter full name"
- required
- aria-required="true"
- />
-
-
-
- Email Address *
-
- setNewEmployee(prev => ({ ...prev, email: e.target.value }))}
- placeholder="email@company.com"
- required
- aria-required="true"
- />
-
-
-
- Phone Number
-
- setNewEmployee(prev => ({ ...prev, phone: e.target.value }))}
- placeholder="+61 4XX XXX XXX"
- />
-
-
-
- Save Learner
-
- setShowAddDrawer(false)} className="flex-1">
- Cancel
-
-
-
-
-
-
- {/* Import Modal */}
-
-
-
- Import Learners
-
- Upload a CSV file to import multiple learners. Maximum file size: 5MB.
-
-
-
-
-
Step 1: Download Template
-
- Download our CSV template with the required fields: Name, Email, Phone.
-
-
-
- Download CSV Template
-
-
-
-
Step 2: Upload File
-
-
-
- Drag and drop your CSV file here, or click to browse
-
-
- Choose File
-
-
-
-
- Import Learners
- setShowImportModal(false)} className="flex-1">
- Cancel
-
-
-
-
-
-
- {/* Assign Modal */}
-
-
-
- Assign to Programme/Course
-
- Assign {selectedEmployees.length} selected learner{selectedEmployees.length !== 1 ? 's' : ''} to a programme or course.
-
-
-
-
-
- Select Programme/Course
-
-
-
-
-
-
- Leadership Development
- Technical Skills
- Communication
- Project Management
-
-
-
-
-
- Start Date (Optional)
-
-
-
-
- Assign
- setShowAssignModal(false)} className="flex-1">
- Cancel
-
-
-
-
-
-
- {/* Edit/Assign Drawer */}
-
-
-
-
- {editingEmployee?.name}
-
-
- Edit learner details and manage course assignments.
-
-
- {editingEmployee && (
-
-
- Details
- Enrolments
-
-
-
-
- Name
-
-
-
-
-
- Phone
-
-
-
-
-
- Status
-
-
-
-
-
-
- Active
- Inactive
- Pending
-
-
-
-
-
-
-
-
Current Enrolments
-
-
- Assign Course
-
-
- {editingEmployee.programme && (
-
-
-
-
-
{editingEmployee.programme}
-
{editingEmployee.course}
- {editingEmployee.progress !== undefined && (
-
- )}
-
-
- Unassign
-
-
-
-
- )}
-
-
-
- Save Changes
- setShowEditDrawer(false)} className="flex-1">
- Cancel
-
-
-
- )}
-
-
-
- );
-};
-
-const TestimonialsTabContent: React.FC = () => {
- const [loading, setLoading] = useState(true);
- const [formData, setFormData] = useState({
- name: 'Alex Sharma',
- email: 'alex.sharma@company.com',
- phone: '',
- organisation: 'Acme Corp',
- programme: '',
- testimonialText: '',
- consentToPublish: false
- });
- const [formErrors, setFormErrors] = useState>({});
- const [isSubmitting, setIsSubmitting] = useState(false);
- const [submitSuccess, setSubmitSuccess] = useState(false);
- const [submitError, setSubmitError] = useState('');
- const [charCount, setCharCount] = useState(0);
- const [prefersReducedMotion] = useLocalStorage('prefersReducedMotion', false);
-
- // Simulate profile pre-fill loading
- useEffect(() => {
- const timer = setTimeout(() => setLoading(false), 300);
- return () => clearTimeout(timer);
- }, []);
-
- const programmes = [
- 'Leadership Development',
- 'Technical Skills',
- 'Communication',
- 'Project Management',
- 'Sales Training'
- ];
-
- const validateForm = () => {
- const errors: Record = {};
-
- if (!formData.testimonialText.trim()) {
- errors.testimonialText = 'Testimonial text is required';
- } else if (formData.testimonialText.length < 1) {
- errors.testimonialText = 'Testimonial must be at least 1 character';
- } else if (formData.testimonialText.length > 2000) {
- errors.testimonialText = 'Testimonial must be 2000 characters or less';
- }
-
- if (!formData.consentToPublish) {
- errors.consentToPublish = 'You must consent to publish your testimonial';
- }
-
- return errors;
- };
-
- const isFormValid = () => {
- const errors = validateForm();
- return Object.keys(errors).length === 0 && formData.testimonialText.trim().length > 0;
- };
-
- const handleInputChange = (field: keyof TestimonialFormData, value: string | boolean) => {
- setFormData(prev => ({ ...prev, [field]: value }));
-
- if (field === 'testimonialText' && typeof value === 'string') {
- setCharCount(value.length);
- }
-
- // Clear field error when user starts typing
- if (formErrors[field]) {
- setFormErrors(prev => {
- const newErrors = { ...prev };
- delete newErrors[field];
- return newErrors;
- });
- }
- };
-
- const handleSubmit = async (e: React.FormEvent) => {
- e.preventDefault();
-
- const errors = validateForm();
- if (Object.keys(errors).length > 0) {
- setFormErrors(errors);
- return;
- }
-
- setIsSubmitting(true);
- setSubmitError('');
-
- try {
- // Simulate API call
- await new Promise(resolve => setTimeout(resolve, 1500));
-
- // Simulate occasional error for testing
- if (Math.random() > 0.9) {
- throw new Error('Submission failed. Please try again.');
- }
-
- setSubmitSuccess(true);
- setFormData(prev => ({ ...prev, testimonialText: '', programme: '', consentToPublish: false }));
- setCharCount(0);
- } catch (error) {
- setSubmitError(error instanceof Error ? error.message : 'An error occurred. Please try again.');
- } finally {
- setIsSubmitting(false);
- }
- };
-
- const resetForm = () => {
- setSubmitSuccess(false);
- setSubmitError('');
- setFormErrors({});
- };
-
- if (loading) {
- return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
- }
-
- return (
-
- {/* Success State */}
- {submitSuccess && (
-
-
-
- Thanks — your testimonial is pending review by KLC.
-
- Submit another testimonial
-
-
-
- )}
-
- {/* Error State */}
- {submitError && (
-
-
-
- {submitError}
-
-
- )}
-
- {/* Testimonial Form */}
-
-
- Submit Testimonial
-
- Share your experience with KLC programmes to help others discover the value of our learning solutions.
-
-
-
-
-
- {/* Your Name */}
-
-
- Your Name
-
-
-
- Pre-filled from your profile
-
-
-
- {/* Work Email */}
-
-
- Work Email
-
-
-
- Pre-filled from your profile
-
-
-
- {/* Phone */}
-
-
- Phone (Optional)
-
- handleInputChange('phone', e.target.value)}
- placeholder="+61 4XX XXX XXX"
- className="min-tap-44"
- />
-
-
- {/* Organisation */}
-
-
- Organisation
-
-
-
- Pre-filled from your profile
-
-
-
-
- {/* Programme */}
-
-
- Programme (Optional)
-
- handleInputChange('programme', value)}
- >
-
-
-
-
- {programmes.map((programme) => (
-
- {programme}
-
- ))}
-
-
-
-
- {/* Testimonial Text */}
-
-
- Testimonial Text *
-
-
handleInputChange('testimonialText', e.target.value)}
- placeholder="Share your experience (1–2000 chars)…"
- className={`min-h-[120px] min-tap-44 ${formErrors.testimonialText ? 'border-status-error' : ''}`}
- aria-invalid={!!formErrors.testimonialText}
- aria-describedby="testimonial-help testimonial-counter testimonial-error"
- maxLength={2000}
- required
- />
-
-
-
- Share what you learned, how it helped, or what you'd recommend to others
-
- {formErrors.testimonialText && (
-
- {formErrors.testimonialText}
-
- )}
-
-
2000 ? 'text-status-error' : 'text-muted-foreground'}`}
- aria-live="polite"
- >
- {charCount}/2000
-
-
-
-
- {/* Consent Checkbox */}
-
-
-
handleInputChange('consentToPublish', !!checked)}
- className={`min-tap-44 mt-1 ${formErrors.consentToPublish ? 'border-status-error' : ''}`}
- aria-invalid={!!formErrors.consentToPublish}
- aria-describedby="consent-label consent-error"
- required
- />
-
-
- I consent to publish this testimonial *
-
-
- Your testimonial may be used in marketing materials and on our website.
- You can request removal at any time by contacting us.
-
- {formErrors.consentToPublish && (
-
- {formErrors.consentToPublish}
-
- )}
-
-
-
-
- {/* Submit Button */}
-
-
- {isSubmitting ? (
- <>
-
- Submitting...
- >
- ) : (
- 'Submit Testimonial'
- )}
-
-
- Your testimonial will be reviewed before being published
-
-
-
-
-
-
- {/* Status Notice */}
-
-
-
-
-
-
- Submissions are reviewed by KLC Super-Admins before appearing publicly.
-
- View policy
-
-
-
-
-
-
-
-
- );
-};
-
-const ReportsScreen: React.FC = () => {
- const [activeTab, setActiveTab] = useState('tracker');
- const [dateRange, setDateRange] = useState('last-30-days');
- const [selectedProgramme, setSelectedProgramme] = useState('all');
- const [selectedCohort, setSelectedCohort] = useState('all');
- const [exporting, setExporting] = useState(false);
- const [loading, setLoading] = useState(false);
-
- const handleExport = async (format: 'csv' | 'pdf' | 'excel') => {
- setExporting(true);
- await new Promise(resolve => setTimeout(resolve, 2000));
- setExporting(false);
- console.log(`Exported as ${format.toUpperCase()}`);
- };
-
- const generateReport = () => {
- setLoading(true);
- setTimeout(() => setLoading(false), 1500);
- };
-
- // Mock data for reports
- const trackerMetrics = [
- { metric: 'Completion Rate', current: 78, previous: 72, trend: 'up' },
- { metric: 'Average Score', current: 85, previous: 82, trend: 'up' },
- { metric: 'Time to Complete', current: 21, previous: 24, trend: 'up' },
- { metric: 'Engagement Rate', current: 92, previous: 89, trend: 'up' }
- ];
-
- const individualReports = [
- { id: '1', name: 'Sarah Chen', programme: 'Leadership Development', progress: 85, lastAccess: '2 hours ago', status: 'On Track' },
- { id: '2', name: 'Michael Rodriguez', programme: 'Technical Skills', progress: 62, lastAccess: '1 day ago', status: 'Behind' },
- { id: '3', name: 'Emma Thompson', programme: 'Communication', progress: 94, lastAccess: '3 hours ago', status: 'Completed' },
- { id: '4', name: 'David Kim', programme: 'Project Management', progress: 78, lastAccess: '5 hours ago', status: 'On Track' },
- { id: '5', name: 'Lisa Wang', programme: 'Leadership Development', progress: 56, lastAccess: '2 days ago', status: 'Behind' }
- ];
-
- const cohortData = [
- { cohort: 'Leadership Q4 2024', enrolled: 45, completed: 32, inProgress: 11, notStarted: 2, avgScore: 87 },
- { cohort: 'Technical Skills Cohort A', enrolled: 38, completed: 25, inProgress: 10, notStarted: 3, avgScore: 82 },
- { cohort: 'Communication Workshop', enrolled: 28, completed: 28, inProgress: 0, notStarted: 0, avgScore: 91 },
- { cohort: 'Project Management Cert', enrolled: 52, completed: 18, inProgress: 28, notStarted: 6, avgScore: 79 }
- ];
-
- return (
-
- {/* Reports Header */}
-
-
-
HR Access Reports
-
Comprehensive reporting and analytics for learning programs
-
-
-
-
-
-
-
- Last 7 days
- Last 30 days
- Last 90 days
- Last year
-
-
- handleExport('excel')}
- disabled={exporting}
- className="min-tap-44"
- >
- {exporting ? (
-
- ) : (
-
- )}
- Export
-
-
-
-
- {/* Reports Tabs */}
-
-
-
-
-
-
- Tracker
-
-
-
- Individual Reports
-
-
-
- Cohort Reports
-
-
-
- {/* Tracker Tab */}
-
-
-
- Learning Progress Tracker
- Real-time metrics and performance indicators
-
-
- {/* Tracker KPIs */}
-
- {trackerMetrics.map((metric, index) => (
-
-
-
-
-
{metric.metric}
-
- {metric.current}
- {metric.metric.includes('Rate') ? '%' : metric.metric.includes('Time') ? ' days' : metric.metric.includes('Score') ? '/100' : ''}
-
-
-
- {metric.trend === 'up' ? '+' : ''}
- {metric.current - metric.previous}
- {metric.metric.includes('Rate') ? '%' : ''}
-
-
-
-
- ))}
-
-
- {/* Progress Chart */}
-
-
-
- Completion Trends
- Weekly completion rates over time
-
-
-
-
-
-
Completion Trends Chart
-
- Line chart showing completion rates over time
-
-
-
-
- Line chart showing completion trends over the selected time period
-
-
-
-
-
-
- Activity Heatmap
- Learning activity by day of week
-
-
-
-
-
-
Activity Heatmap
-
- Visual representation of learning activity patterns
-
-
-
-
- Heatmap showing learning activity patterns by day and time
-
-
-
-
-
-
-
-
- {/* Individual Reports Tab */}
-
-
-
-
-
- Individual Learner Reports
- Detailed progress reports for each learner
-
-
-
-
-
-
-
- All Programmes
- Leadership Development
- Technical Skills
- Communication
- Project Management
-
-
-
- {loading ? (
-
- ) : (
-
- )}
- Generate Report
-
-
-
-
-
-
-
-
-
- Learner
- Programme
- Progress
- Last Access
- Status
- Actions
-
-
-
- {individualReports.map((learner) => (
-
- {learner.name}
- {learner.programme}
-
-
-
-
{learner.progress}%
-
-
- {learner.lastAccess}
-
-
- {learner.status}
-
-
-
-
-
-
-
-
- ))}
-
-
-
-
-
-
-
- {/* Cohort Reports Tab */}
-
-
-
-
-
- Cohort Performance Reports
- Group-level analytics and performance metrics
-
-
-
-
-
-
-
- All Cohorts
- Leadership Q4 2024
- Technical Skills Cohort A
- Communication Workshop
- Project Management Cert
-
-
- handleExport('pdf')}
- disabled={exporting}
- className="min-tap-44"
- >
- {exporting ? (
-
- ) : (
-
- )}
- Export PDF
-
-
-
-
-
-
- {/* Cohort Summary Cards */}
- {cohortData.map((cohort, index) => (
-
-
-
-
-
{cohort.cohort}
-
{cohort.enrolled} total learners enrolled
-
-
-
-
-
{cohort.completed}
-
Completed
-
-
-
{cohort.inProgress}
-
In Progress
-
-
-
{cohort.notStarted}
-
Not Started
-
-
-
{cohort.avgScore}/100
-
Avg Score
-
-
-
-
- {/* Progress Bar */}
-
-
- Completion Progress
- {Math.round((cohort.completed / cohort.enrolled) * 100)}%
-
-
-
-
-
- ))}
-
-
-
-
-
-
-
-
- );
-};
-
-const ProfileScreen: React.FC = () => {
- const [activeTab, setActiveTab] = useState('organisation');
-
- return (
-
- {/* Profile Tabs */}
-
-
-
-
-
-
- Organisation
- Org
-
-
-
- Billing
-
-
-
- Roles
-
-
-
- Testimonials
- Reviews
-
-
-
-
-
-
- Organisation Profile
- Manage your organisation's profile and settings
-
-
-
-
-
Organisation profile settings would be displayed here
-
-
-
-
-
-
-
-
- Billing & Subscriptions
- Manage your billing information and subscription plans
-
-
-
-
-
Billing and subscription management would be displayed here
-
-
-
-
-
-
-
-
- Roles & Permissions
- Manage user roles and access permissions
-
-
-
-
-
Roles and permissions management would be displayed here
-
-
-
-
-
-
-
-
-
-
-
-
- );
-};
-
-// Main App Component
-export default function App() {
- const [currentScreen, setCurrentScreen] = useState('home');
- const [isDark, setIsDark] = useLocalStorage('darkMode', false);
- const [prefersReducedMotion, setPrefersReducedMotion] = useLocalStorage('prefersReducedMotion', false);
- const [sidebarOpen, setSidebarOpen] = useState(false);
- const [announcementsOpen, setAnnouncementsOpen] = useState(false);
- const [screenFilters, setScreenFilters] = useState({});
-
- // Apply theme
- useEffect(() => {
- document.documentElement.classList.toggle('dark', isDark);
- }, [isDark]);
-
- // Handle navigation
- const handleNavigate = useCallback((screen: string, filters?: any) => {
- setCurrentScreen(screen);
-
- if (filters) {
- setScreenFilters(prev => ({ ...prev, [screen]: filters }));
- }
- setSidebarOpen(false); // Close sidebar on mobile after navigation
- }, []);
-
- const handleNotificationToggle = () => {
- setAnnouncementsOpen(!announcementsOpen);
- };
-
- const handleMarkAsRead = (id: string) => {
- console.log(`Marked notification ${id} as read`);
- };
-
- const renderCurrentScreen = () => {
- const filters = screenFilters[currentScreen];
-
- switch (currentScreen) {
- case 'home':
- return ;
- case 'learners':
- return ;
- case 'reports':
- return ;
- case 'profile':
- return ;
- default:
- return ;
- }
- };
-
- return (
-
- {/* Skip to main content link for accessibility */}
-
- Skip to main content
-
-
- {/* Top Navigation */}
-
setSidebarOpen(!sidebarOpen)}
- showMenuButton={true}
- onNotificationToggle={handleNotificationToggle}
- notificationCount={mockAnnouncements.length}
- />
-
-
- {/* Desktop Sidebar */}
-
-
- {/* Mobile Sidebar Overlay */}
- {sidebarOpen && (
-
-
setSidebarOpen(false)}
- />
-
-
- )}
-
- {/* Main Content */}
-
-
-
- {renderCurrentScreen()}
-
-
-
-
- {/* Announcements Panel */}
-
setAnnouncementsOpen(false)}
- announcements={mockAnnouncements}
- onMarkAsRead={handleMarkAsRead}
- />
-
- {/* Chat Bot FAB */}
-
-
- {/* Footer */}
-
-
- );
-}
\ No newline at end of file
diff --git a/src/components/BreadcrumbNav.tsx b/src/components/BreadcrumbNav.tsx
new file mode 100644
index 0000000..b30ceca
--- /dev/null
+++ b/src/components/BreadcrumbNav.tsx
@@ -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 = {
+ '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 (
+
+
+ {/* Home icon */}
+
+ navigate('/hr/dashboard')}
+ className="text-muted-foreground hover:text-foreground transition-colors p-1"
+ aria-label="Go to dashboard"
+ >
+
+
+
+
+ {pathSegments.map((segment, index) => {
+ const isLast = index === pathSegments.length - 1;
+ const path = '/' + pathSegments.slice(0, index + 1).join('/');
+ const displayName = getDisplayName(segment);
+
+ return (
+
+
+ {isLast ? (
+
+ {displayName}
+
+ ) : (
+ navigate(path)}
+ className="text-muted-foreground hover:text-foreground transition-colors"
+ >
+ {displayName}
+
+ )}
+
+ );
+ })}
+
+
+ );
+};
+
+export default BreadcrumbNav;
\ No newline at end of file
diff --git a/src/components/TopNav.tsx b/src/components/TopNav.tsx
new file mode 100644
index 0000000..aa6b5a3
--- /dev/null
+++ b/src/components/TopNav.tsx
@@ -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 = (key: string, initialValue: T): [T, (value: T) => void] => {
+ const [storedValue, setStoredValue] = React.useState(() => {
+ 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 = ({
+ onMenuToggle,
+ showMenuButton = false,
+ onNotificationToggle,
+ notificationCount = 0
+}) => {
+ const navigate = useNavigate();
+ const [preferences, setPreferences] = useLocalStorage('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 (
+
+ {/* Left Section */}
+
+ {showMenuButton && (
+
+
+
+ )}
+
+ {/* Logo and Brand */}
+
+
+
+
HR Dashboard
+
Knowledge Learning Centre
+
+
+
+
+ {/* Right Section */}
+
+ {/* Notifications */}
+
+
+ {notificationCount > 0 && (
+
+ {notificationCount > 9 ? '9+' : notificationCount}
+
+ )}
+
+
+ {/* Theme Toggle */}
+
+ {preferences.darkMode ? : }
+
+
+ {/* Profile Dropdown */}
+
+
+
+
+
+ HR
+
+
+
+
HR Manager
+
hr@klc.com
+
+
+
+
+
+
+ {/* User Profile Section */}
+
+
+
+ HR
+
+
+
+
HR Manager
+
hr@klc.com
+
+ Administrator
+
+
+
+
+ {/* Quick Stats */}
+
+
+
Active Learners
+
247
+
+
+
+
+ {/* Switch Mode Section */}
+
+
SWITCH MODE
+
+ handleSwitchMode('learning')}
+ >
+
+ Learning
+
+ handleSwitchMode('hr')}
+ >
+
+ HR Mode
+
+
+
+
+ {/* Menu Items */}
+
+
+
+
+
My Profile
+
View and edit your profile
+
+
+
+
+
+
+
Settings
+
Manage your preferences
+
+
+
+
+
+
+
Help & Support
+
Get help and documentation
+
+
+
+
+
+
+ {/* Sign out */}
+
+
+
+
Sign out
+
End your session
+
+
+
+ {/* Footer */}
+
+
+
+
+ {/* Mobile Theme Toggle (visible only on small screens) */}
+
+ {preferences.darkMode ? : }
+
+
+
+ );
+};
+
+export default TopNav;
\ No newline at end of file
diff --git a/src/components/shared/ChatBot.tsx b/src/components/shared/ChatBot.tsx
new file mode 100644
index 0000000..e362bc5
--- /dev/null
+++ b/src/components/shared/ChatBot.tsx
@@ -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 = ({ 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 (
+
+ {isOpen && (
+
+
+
HR Assistant
+ setIsOpen(false)}
+ className="h-6 w-6"
+ aria-label="Close chat"
+ >
+
+
+
+
+ {chips.map((chip, index) => (
+
+ {chip}
+
+ ))}
+
+
+ )}
+
setIsOpen(!isOpen)}
+ className="rounded-full h-12 w-12 shadow-lg min-tap-44"
+ aria-label="Open HR chat assistant"
+ aria-expanded={isOpen}
+ >
+
+
+
+ );
+};
\ No newline at end of file
diff --git a/src/components/shared/KPICard.tsx b/src/components/shared/KPICard.tsx
new file mode 100644
index 0000000..e05e5bb
--- /dev/null
+++ b/src/components/shared/KPICard.tsx
@@ -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 = ({
+ isOpen,
+ onClose,
+ announcements,
+ onMarkAsRead
+}) => {
+ if (!isOpen) return null;
+
+ return (
+
+
+
Announcements & Reminders
+
+
+
+
+
+
+ {announcements.map((item) => (
+
onMarkAsRead?.(item.id)}
+ >
+
+
+
+ {item.type === 'announcement' ?
+ :
+
+ }
+
+ {item.pinned && (
+
Pinned
+ )}
+
+
{item.timestamp}
+
+
+
{item.title}
+
{item.content}
+
+
+
+ {item.type}
+
+
+ Mark as read
+
+
+
+ ))}
+
+
+
+
+ View All Notifications
+
+
+
+ );
+};
\ No newline at end of file
diff --git a/src/components/ui/navigation-menu.tsx b/src/components/ui/navigation-menu.tsx
index a4a30ed..6a8d6f2 100644
--- a/src/components/ui/navigation-menu.tsx
+++ b/src/components/ui/navigation-menu.tsx
@@ -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";
diff --git a/src/hooks/useCountUp.ts b/src/hooks/useCountUp.ts
new file mode 100644
index 0000000..00facf5
--- /dev/null
+++ b/src/hooks/useCountUp.ts
@@ -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;
+}
\ No newline at end of file
diff --git a/src/hooks/useLocalStorage.ts b/src/hooks/useLocalStorage.ts
new file mode 100644
index 0000000..774e0bf
--- /dev/null
+++ b/src/hooks/useLocalStorage.ts
@@ -0,0 +1,24 @@
+import { useState, useEffect } from 'react';
+
+export function useLocalStorage(key: string, initialValue: T): [T, (value: T) => void] {
+ const [storedValue, setStoredValue] = useState(() => {
+ 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];
+}
\ No newline at end of file
diff --git a/src/layouts/HRLayout.tsx b/src/layouts/HRLayout.tsx
new file mode 100644
index 0000000..59daafc
--- /dev/null
+++ b/src/layouts/HRLayout.tsx
@@ -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 (
+
+ {/* Skip to main content link for accessibility */}
+
+ Skip to main content
+
+
+ {/* Top Navigation */}
+
setSidebarOpen(!sidebarOpen)}
+ showMenuButton={true}
+ onNotificationToggle={handleNotificationToggle}
+ notificationCount={mockAnnouncements.length}
+ />
+
+
+ {/* Desktop Sidebar */}
+
+
+ {/* Mobile Sidebar Overlay */}
+ {sidebarOpen && (
+
+ {/* Backdrop */}
+
setSidebarOpen(false)}
+ aria-hidden="true"
+ />
+
+ {/* Sidebar */}
+
+ setSidebarOpen(false)}
+ />
+
+
+ )}
+
+ {/* Main Content */}
+
+
+
+
+
+ {/* Announcements Panel */}
+
setAnnouncementsOpen(false)}
+ announcements={mockAnnouncements}
+ onMarkAsRead={handleMarkAsRead}
+ />
+
+ {/* Chat Bot FAB */}
+
+
+ {/* Footer */}
+
+
+ );
+};
+
+export default HRLayout;
\ No newline at end of file
diff --git a/src/layouts/components/BreadcrumbNav.tsx b/src/layouts/components/BreadcrumbNav.tsx
new file mode 100644
index 0000000..51b17b5
--- /dev/null
+++ b/src/layouts/components/BreadcrumbNav.tsx
@@ -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 (
+
+
+
+
+ HR Portal
+
+
+
+ {pathnames.map((name, index) => {
+ const routeTo = `/${pathnames.slice(0, index + 1).join('/')}`;
+ const isLast = index === pathnames.length - 1;
+
+ if (name === 'hr') return null;
+
+ return (
+
+
+
+ {isLast ? (
+ {getBreadcrumbName(name)}
+ ) : (
+
+ {getBreadcrumbName(name)}
+
+ )}
+
+
+ );
+ })}
+
+
+ );
+};
\ No newline at end of file
diff --git a/src/layouts/components/HRSidebar.tsx b/src/layouts/components/HRSidebar.tsx
new file mode 100644
index 0000000..0856d3d
--- /dev/null
+++ b/src/layouts/components/HRSidebar.tsx
@@ -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 = ({ className = '', onNavigate }) => {
+ const [prefersReducedMotion] = useLocalStorage('prefersReducedMotion', false);
+
+ return (
+
+
+
+
+ TS
+
+
Tech Solutions Pvt Ltd
+
+
+
+
+
+ {menuItems.map((item) => {
+ const Icon = item.icon;
+
+ return (
+
+ `
+ 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}`}
+ >
+
+ {item.label}
+
+
+ );
+ })}
+
+
+
+ );
+};
\ No newline at end of file
diff --git a/src/main.tsx b/src/main.tsx
index d96f8d8..b3c11a7 100644
--- a/src/main.tsx
+++ b/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( );
-
\ No newline at end of file
+ReactDOM.createRoot(document.getElementById('root')!).render(
+
+
+
+);
\ No newline at end of file
diff --git a/src/pages/CourseViewPage/CourseViewPage.tsx b/src/pages/CourseViewPage/CourseViewPage.tsx
new file mode 100644
index 0000000..c645e9c
--- /dev/null
+++ b/src/pages/CourseViewPage/CourseViewPage.tsx
@@ -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(null);
+ const [loading, setLoading] = useState(true);
+ const [activeTab, setActiveTab] = useState('overview');
+ const [selectedModule, setSelectedModule] = useState(null);
+
+ useEffect(() => {
+ // Simulate API call
+ const timer = setTimeout(() => {
+ setCourse(mockCourse);
+ setSelectedModule(mockCourse.modules[0]);
+ setLoading(false);
+ }, 500);
+
+ return () => clearTimeout(timer);
+ }, [id]);
+
+ if (loading) {
+ return (
+
+ );
+ }
+
+ if (!course) {
+ return (
+
+
+
Course not found
+
The course you're looking for doesn't exist.
+
navigate('/dashboard')}>Back to Dashboard
+
+ );
+ }
+
+ const getModuleIcon = (type: string) => {
+ switch (type) {
+ case 'video': return ;
+ case 'reading': return ;
+ case 'quiz': return ;
+ case 'assignment': return ;
+ default: return ;
+ }
+ };
+
+ return (
+
+ {/* Header */}
+
+
+
navigate(`/programme/${course.programmeId}`)}
+ className="min-tap-44"
+ >
+
+ Back to Programme
+
+
+
+ {course.programmeName}
+
+ {course.title}
+
+
{course.title}
+
+
+
+
+
+ Save
+
+
+
+ Share
+
+
+
+
+ {/* Course Stats */}
+
+
+
+
+
+
Your Progress
+
{course.progress}%
+
+
+
+
+
+
+
+
+
+
Duration
+
{course.duration}
+
+
+
+
+
+
+
+
+
+
Enrolled
+
{course.enrolledCount}
+
+
+
+
+
+
+
+
+
+
Rating
+
{course.rating}/5.0
+
+
+
+
+
+
+
+ {/* Main Content */}
+
+ {/* Course Content */}
+
+ {/* Course Info */}
+
+
+ About this Course
+
+
+ {course.description}
+
+
+
+
+
+ {course.instructor.name.split(' ').map(n => n[0]).join('')}
+
+
+
+
{course.instructor.name}
+
{course.instructor.title}
+
+
+
{course.status}
+
+
+
+
+ {/* Course Modules */}
+
+
+ Course Modules
+ {course.modules.length} modules • {course.duration} total
+
+
+
+ {course.modules.map((module, index) => (
+
setSelectedModule(module)}
+ >
+
+
+ {module.status === 'completed' ? (
+
+ ) : (
+ getModuleIcon(module.type)
+ )}
+
+
+
+
+
{module.title}
+
+ {module.status}
+
+
+
+
+
+
+ {module.duration}
+
+ •
+ {module.type}
+
+
+
+
+ ))}
+
+
+
+
+ {/* Discussions Preview */}
+
+
+ Recent Discussions
+ Join the conversation
+
+
+
+ {course.discussions.map(discussion => (
+
+
+
+
+ {discussion.user.name.split(' ').map(n => n[0]).join('')}
+
+
+
+
+ {discussion.user.name}
+
+ {discussion.timestamp}
+
+
+
{discussion.content}
+
+
+
+ {discussion.likes}
+
+
+
+ {discussion.replies} replies
+
+
+
+
+
+ ))}
+
+
+
+
+
+ {/* Current Module View */}
+
+
+
+ Current Module
+
+ {selectedModule?.title}
+
+
+
+ {selectedModule && (
+ <>
+
+ {selectedModule.type === 'video' ? (
+
+ ) : (
+
+ )}
+
+
+
+
+ Type:
+ {selectedModule.type}
+
+
+ Duration:
+ {selectedModule.duration}
+
+
+ Status:
+
+ {selectedModule.status}
+
+
+
+
+
+ {selectedModule.status === 'completed' ? 'Review Module' :
+ selectedModule.status === 'available' ? 'Start Module' :
+ 'Locked'}
+
+
+ {/* Resources */}
+
+ >
+ )}
+
+
+
+
+
+ );
+};
+
+export default CourseViewPage;
\ No newline at end of file
diff --git a/src/pages/Dashboard/DashboardPage.tsx b/src/pages/Dashboard/DashboardPage.tsx
new file mode 100644
index 0000000..f38bb56
--- /dev/null
+++ b/src/pages/Dashboard/DashboardPage.tsx
@@ -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 (
+
+
+ {[...Array(4)].map((_, i) => (
+
+
+
+
+
+
+
+
+ ))}
+
+
+
+
+
+
+
+
+
+
+ );
+ }
+
+ return (
+
+ {/* Welcome Section */}
+
+
+
Welcome Priya 👋
+
Manage programmes, track progress, and stay connected with your learning community
+
+
+
+ {/* KPI Cards */}
+
+ {mockKPIData.map((kpi, index) => (
+
handleNavigate('/hr/reports')}
+ className={prefersReducedMotion ? '' : 'animate-slide-up'}
+ style={{ animationDelay: `${index * 100}ms` }}
+ />
+ ))}
+
+
+ {/* Quick Actions Section */}
+
+
+ Quick Actions
+ Common HR tasks and shortcuts
+
+
+
+ {[
+ { 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 (
+
+
+ {link.title}
+
+ );
+ })}
+
+
+
+
+ {/* Programmes Table */}
+
+
console.log(`Assign learners to: ${programmeId}`)}
+ onDownloadTracker={(programmeId) => console.log(`Download tracker for: ${programmeId}`)}
+ userAccessLevel={getUserAccessLevel()}
+ />
+
+
+ {/* Programme Schedule - Horizontal layout below programmes */}
+
+
console.log(`Open event: ${event.title}`)}
+ />
+
+
+ {/* Learning Analytics - Full Width */}
+
+ handleNavigate('/hr/learners', { editEmployee: learnerId })}
+ onNudgeLearner={(learnerId) => console.log(`Nudge learner: ${learnerId}`)}
+ onViewAllAnalytics={(programmeId) => handleNavigate('/hr/reports', { programme: programmeId })}
+ />
+
+
+ {/* Discussion Forum Feed */}
+
+ handleNavigate('/hr/discussions', { thread: threadId })}
+ onMarkAsRead={(threadId) => console.log(`Mark as read: ${threadId}`)}
+ />
+
+
+ );
+};
+
+export default DashboardPage;
\ No newline at end of file
diff --git a/src/pages/DiscussionsPage/DiscussionsPage.tsx b/src/pages/DiscussionsPage/DiscussionsPage.tsx
new file mode 100644
index 0000000..b6973f7
--- /dev/null
+++ b/src/pages/DiscussionsPage/DiscussionsPage.tsx
@@ -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(null);
+ const [showNewThreadModal, setShowNewThreadModal] = useState(false);
+ const [newThread, setNewThread] = useState({ title: '', content: '', category: '', tags: '' });
+ const [replyContent, setReplyContent] = useState('');
+ const [bookmarkedThreads, setBookmarkedThreads] = useState([]);
+
+ 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 (
+
+ {/* Header */}
+
+
+
Discussion Forums
+
Connect, share, and learn with your peers
+
+
setShowNewThreadModal(true)} className="min-tap-44">
+
+ New Discussion
+
+
+
+ {/* Stats Cards */}
+
+
+
+
+
+
Total Discussions
+
{mockThreads.length}
+
+
+
+
+
+
+
+
+
+
Active Today
+
+ {mockThreads.filter(t => {
+ const lastActivity = new Date(t.lastActivity);
+ const today = new Date();
+ return lastActivity.toDateString() === today.toDateString();
+ }).length}
+
+
+
+
+
+
+
+
+
+
+
Total Replies
+
+ {mockThreads.reduce((acc, t) => acc + t.replies, 0)}
+
+
+
+
+
+
+
+
+
+
+
Solved
+
+ {mockThreads.filter(t => t.isSolved).length}
+
+
+
+
+
+
+
+
+ {viewMode === 'list' ? (
+ <>
+ {/* Search and Filters */}
+
+
+
+
+
+
+ setSearchTerm(e.target.value)}
+ className="pl-10"
+ />
+
+
+
+
+
+
+ Latest
+ Most Active
+ Most Viewed
+
+
+
+
+
+
+ Filters
+
+
+
+
+
+
+ {/* Categories and Threads */}
+
+ {/* Categories Sidebar */}
+
+
+ Categories
+
+
+
+ {categories.map(category => (
+ 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'
+ }`}
+ >
+ {category.name}
+
+ {category.count}
+
+
+ ))}
+
+
+
+
+ {/* Threads List */}
+
+ {filteredThreads.map(thread => (
+
{
+ setSelectedThread(thread);
+ setViewMode('detail');
+ }}
+ >
+
+
+
+
+ {thread.author.name.split(' ').map(n => n[0]).join('')}
+
+
+
+
+
+ {thread.isPinned && (
+
+ )}
+ {thread.isSolved && (
+
+ )}
+
{thread.title}
+
+
+
+ {thread.content}
+
+
+
+
+ {thread.author.name}
+
+ •
+ {thread.author.role}
+ •
+ {formatDate(thread.createdAt)}
+
+
+
+ {thread.tags.map(tag => (
+
+ #{tag}
+
+ ))}
+
+
+
+
+
+
+
+ {thread.replies}
+
+
+
+ {thread.views}
+
+
+
+ {thread.likes}
+
+
+
+ Last activity {formatDate(thread.lastActivity)}
+
+
+
+
+
+ ))}
+
+
+ >
+ ) : (
+ /* Thread Detail View */
+ selectedThread && (
+
+ {/* Back Button */}
+
setViewMode('list')}
+ className="min-tap-44"
+ >
+
+ Back to Discussions
+
+
+ {/* Main Thread */}
+
+
+
+
+
+ {selectedThread.author.name.split(' ').map(n => n[0]).join('')}
+
+
+
+
+
+
+
+ {selectedThread.isPinned && (
+
+ )}
+ {selectedThread.isSolved && (
+
+ )}
+
{selectedThread.title}
+
+
+
+
+ {selectedThread.author.name}
+
+ •
+ {selectedThread.author.role}
+ •
+ {formatDate(selectedThread.createdAt)}
+
+
+
+
+ toggleBookmark(selectedThread.id)}
+ className="min-tap-44"
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
{selectedThread.content}
+
+
+
+ {selectedThread.tags.map(tag => (
+
+ #{tag}
+
+ ))}
+
+
+
+
+
+ Like ({selectedThread.likes})
+
+
+
+ Reply
+
+
+ {selectedThread.views} views
+
+
+
+
+
+
+
+ {/* Replies */}
+
+
+ Replies ({threadReplies.length})
+
+
+
+ {threadReplies.map(reply => (
+
+
+
+
+ {reply.author.name.split(' ').map(n => n[0]).join('')}
+
+
+
+
+
+
+ {reply.author.name}
+
+ {reply.author.role}
+
+
+ {formatDate(reply.createdAt)}
+
+ {reply.isBestAnswer && (
+
+ Best Answer
+
+ )}
+
+
+
+
+
+
+
+
{reply.content}
+
+
+
+
+ {reply.likes}
+
+
+
+ Reply
+
+
+
+
+
+ ))}
+
+
+
+
+ {/* Add Reply */}
+
+
+ Add Reply
+
+
+
+
setReplyContent(e.target.value)}
+ placeholder="Share your thoughts or answer the question..."
+ className="min-h-[100px]"
+ />
+
+
+
+ Post Reply
+
+
+
+
+
+
+ )
+ )}
+
+ {/* New Thread Modal */}
+
+
+
+ Start New Discussion
+
+ Share your thoughts, ask a question, or start a conversation with the community.
+
+
+
+
+ Title *
+ setNewThread(prev => ({ ...prev, title: e.target.value }))}
+ placeholder="What would you like to discuss?"
+ maxLength={120}
+ />
+
+
+
+ Category *
+ setNewThread(prev => ({ ...prev, category: value }))}>
+
+
+
+
+ Best Practices
+ Advice
+ Recommendations
+ Questions
+
+
+
+
+
+ Content *
+ setNewThread(prev => ({ ...prev, content: e.target.value }))}
+ placeholder="Share your thoughts in detail..."
+ className="min-h-[150px]"
+ />
+
+
+
+ Tags (comma separated)
+ setNewThread(prev => ({ ...prev, tags: e.target.value }))}
+ placeholder="leadership, communication, remote-work"
+ />
+
+
+
+
+ Create Discussion
+
+ setShowNewThreadModal(false)} className="flex-1">
+ Cancel
+
+
+
+
+
+
+ );
+};
+
+export default DiscussionsPage;
\ No newline at end of file
diff --git a/src/pages/Learners/LearnersPage.tsx b/src/pages/Learners/LearnersPage.tsx
new file mode 100644
index 0000000..fa05441
--- /dev/null
+++ b/src/pages/Learners/LearnersPage.tsx
@@ -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(mockEmployees);
+ const [searchTerm, setSearchTerm] = useState('');
+ const [statusFilter, setStatusFilter] = useState('all');
+ const [programmeFilter, setProgrammeFilter] = useState('all');
+ const [selectedEmployees, setSelectedEmployees] = useState([]);
+ const [showAddDrawer, setShowAddDrawer] = useState(false);
+ const [showImportModal, setShowImportModal] = useState(false);
+ const [showAssignModal, setShowAssignModal] = useState(false);
+ const [showEditDrawer, setShowEditDrawer] = useState(false);
+ const [editingEmployee, setEditingEmployee] = useState(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 (
+
+ {/* Header */}
+
+
+
Learners
+
Manage and track all learners in your organization
+
+
+ setViewMode(viewMode === 'table' ? 'grid' : 'table')}
+ className="min-tap-44"
+ >
+ {viewMode === 'table' ? 'Grid View' : 'Table View'}
+
+
+
+
+ {/* Stats Cards */}
+
+
+
+
+
+
Total Learners
+
{employees.length}
+
+
+
+
+
+
+
+
+
+
Active Now
+
{employees.filter(e => e.status === 'Active').length}
+
+
+
+
+
+
+
+
+
+
Pending
+
{employees.filter(e => e.status === 'Pending').length}
+
+
+
+
+
+
+
+
+
+
Avg. Progress
+
+ {Math.round(employees.reduce((acc, e) => acc + (e.progress || 0), 0) / employees.length)}%
+
+
+
+
+
+
+
+
+ {/* Toolbar */}
+
+
+
+
+
+
+ debouncedSearch(e.target.value)}
+ aria-label="Search learners by name or email"
+ />
+
+
+
+
+
+
+
+ All Status
+ Active
+ Inactive
+ Pending
+
+
+
+
+
+
+
+
+ All Programmes
+ {programmes.map(prog => (
+ {prog}
+ ))}
+
+
+
+
+
setShowAddDrawer(true)}
+ className="min-tap-44"
+ >
+
+ Add Learner
+
+
setShowImportModal(true)}
+ className="min-tap-44"
+ >
+
+ Import
+
+
handleExport('excel')}
+ disabled={loading}
+ className="min-tap-44"
+ >
+ {loading ? (
+
+ ) : (
+
+ )}
+ Export
+
+
+
+
+
+
+ {/* Bulk Action Bar */}
+ {bulkActionVisible && (
+
+
+
+
+ {selectedEmployees.length} learner{selectedEmployees.length !== 1 ? 's' : ''} selected
+
+
+ setShowAssignModal(true)}
+ className="min-tap-44"
+ >
+
+ Assign to Programme
+
+
+
+ Send Email
+
+
+
+ Deactivate
+
+
+
+
+
+ )}
+
+ {/* Learners Table/Grid */}
+
+
+
+
+ Learners ({filteredEmployees.length})
+
+ {viewMode === 'table' ? 'Manage learner accounts and assignments' : 'Grid view of all learners'} •
+ Showing {startIndex + 1}-{Math.min(endIndex, filteredEmployees.length)} of {filteredEmployees.length} learners
+
+
+
+ {
+ setItemsPerPage(Number(value));
+ setCurrentPage(1);
+ setSelectedEmployees([]);
+ }}>
+
+
+
+
+ 5
+ 10
+ 20
+ 50
+
+
+ handleBulkSelect(selectedEmployees.length !== paginatedEmployees.length)}
+ className="min-tap-44"
+ >
+ {selectedEmployees.length === paginatedEmployees.length && paginatedEmployees.length > 0 ? 'Deselect All' : 'Select All'}
+
+
+
+
+
+ {viewMode === 'table' ? (
+
+
+
+
+ Select
+ Name
+ Email
+ Phone
+ Programme
+ Progress
+ Status
+ Last Activity
+ Actions
+
+
+
+ {paginatedEmployees.map((employee) => (
+
+
+ handleEmployeeSelect(employee.id, !!checked)}
+ className="min-tap-44"
+ aria-label={`Select ${employee.name}`}
+ />
+
+ {employee.name}
+ {employee.email}
+ {employee.phone}
+ {employee.programme || '-'}
+
+
+
+
{employee.progress}%
+
+
+
+
+ {employee.status}
+
+
+
+ {employee.lastActivity}
+
+
+
+ handleEditEmployee(employee)}
+ className="min-tap-44"
+ aria-label={`Edit ${employee.name}`}
+ >
+
+
+
+
+
+
+
+
+ ))}
+
+
+
+ ) : (
+
+ {paginatedEmployees.map((employee) => (
+
+
+
+
+
+
+ {employee.name.split(' ').map(n => n[0]).join('')}
+
+
+
+
{employee.name}
+
{employee.email}
+
+
+
handleEmployeeSelect(employee.id, !!checked)}
+ className="min-tap-44"
+ />
+
+
+
+
+
+
+ {employee.programme || 'No programme assigned'}
+
+
+
+
+ Progress
+ {employee.progress}%
+
+
+
+
+
+
+ {employee.status}
+
+
+ {employee.lastActivity}
+
+
+
+
+
+ ))}
+
+ )}
+
+ {/* Pagination */}
+ {totalPages > 1 && (
+
+
+ Page {currentPage} of {totalPages}
+
+
+ handlePageChange(currentPage - 1)}
+ disabled={currentPage === 1}
+ className="min-tap-44"
+ >
+
+
+ {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 (
+ handlePageChange(pageNum)}
+ className="min-tap-44"
+ >
+ {pageNum}
+
+ );
+ })}
+ handlePageChange(currentPage + 1)}
+ disabled={currentPage === totalPages}
+ className="min-tap-44"
+ >
+
+
+
+
+ )}
+
+
+
+ {/* Add Learner Drawer */}
+
+
+
+ Add New Learner
+
+ Add a new learner to the system. Email cannot be changed after saving.
+
+
+
+
+
+ Full Name *
+
+ setNewEmployee(prev => ({ ...prev, name: e.target.value }))}
+ placeholder="Enter full name"
+ required
+ />
+
+
+
+ Email Address *
+
+ setNewEmployee(prev => ({ ...prev, email: e.target.value }))}
+ placeholder="email@company.com"
+ required
+ />
+
+
+
+ Phone Number
+
+ setNewEmployee(prev => ({ ...prev, phone: e.target.value }))}
+ placeholder="+61 4XX XXX XXX"
+ />
+
+
+
+ Save Learner
+
+ setShowAddDrawer(false)} className="flex-1">
+ Cancel
+
+
+
+
+
+
+ {/* Import Modal */}
+
+
+
+ Import Learners
+
+ Upload a CSV file to import multiple learners. Maximum file size: 5MB.
+
+
+
+
+
Step 1: Download Template
+
+ Download our CSV template with the required fields: Name, Email, Phone.
+
+
+
+ Download CSV Template
+
+
+
+
Step 2: Upload File
+
+
+
+ Drag and drop your CSV file here, or click to browse
+
+
+ Choose File
+
+
+
+
+ Import Learners
+ setShowImportModal(false)} className="flex-1">
+ Cancel
+
+
+
+
+
+
+ {/* Assign Modal */}
+
+
+
+ Assign to Programme
+
+ Assign {selectedEmployees.length} selected learner{selectedEmployees.length !== 1 ? 's' : ''} to a programme.
+
+
+
+
+
+ Select Programme
+
+
+
+
+
+
+ Leadership Development
+ Technical Skills
+ Communication
+ Project Management
+
+
+
+
+
+ Start Date
+
+
+
+
+
+ End Date
+
+
+
+
+ Assign
+ setShowAssignModal(false)} className="flex-1">
+ Cancel
+
+
+
+
+
+
+ {/* Edit Drawer */}
+
+
+
+ Edit Learner
+
+ Update learner information and manage assignments.
+
+
+ {editingEmployee && (
+
+
+ Details
+ Progress
+ Assignments
+
+
+
+
+ Name
+
+
+
+ Email
+
+
+
+ Phone
+
+
+
+ Status
+
+
+
+
+
+ Active
+ Inactive
+ Pending
+
+
+
+
+
+
+
+
Current Progress
+
+
+
{editingEmployee.progress}%
+
+
+
+
Last Activity
+
{editingEmployee.lastActivity}
+
+
+
+
+ {editingEmployee.programme ? (
+
+
+
+
+
{editingEmployee.programme}
+
{editingEmployee.course}
+
+
Change
+
+
+
+ ) : (
+
+
+
No programmes assigned
+
Assign Programme
+
+ )}
+
+
+
+ Save Changes
+ setShowEditDrawer(false)} className="flex-1">
+ Cancel
+
+
+
+ )}
+
+
+
+ );
+};
+
+export default LearnersPage;
\ No newline at end of file
diff --git a/src/pages/ProgrammeViewPage/ProgrammeViewPage.tsx b/src/pages/ProgrammeViewPage/ProgrammeViewPage.tsx
new file mode 100644
index 0000000..34c97ab
--- /dev/null
+++ b/src/pages/ProgrammeViewPage/ProgrammeViewPage.tsx
@@ -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(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 (
+
+ );
+ }
+
+ if (!programme) {
+ return (
+
+
+
Programme not found
+
The programme you're looking for doesn't exist.
+
navigate('/dashboard')}>Back to Dashboard
+
+ );
+ }
+
+ 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 ;
+ case 'reading': return ;
+ case 'quiz': return ;
+ case 'assignment': return ;
+ default: return ;
+ }
+ };
+
+ return (
+
+ {/* Header */}
+
+
+
navigate(-1)}
+ className="min-tap-44"
+ >
+
+ Back
+
+
+
{programme.name}
+
Programme Overview
+
+
+
+
+
+ Edit
+
+
+
+ Share
+
+
+
+ Delete
+
+
+
+
+ {/* Programme Stats */}
+
+
+
+
+
+
Enrolled
+
+ {programme.enrolledCount}/{programme.capacity}
+
+
+
+
+
+
+
+
+
+
+
Completion Rate
+
{programme.completionRate}%
+
+
+
+
+
+
+
+
+
+
Average Score
+
{programme.averageScore}/100
+
+
+
+
+
+
+
+
+
+
Duration
+
{programme.duration}
+
+
+
+
+
+
+
+ {/* Main Content Tabs */}
+
+
+
+
+ Overview
+ Modules
+ Learners
+ Resources
+
+
+ {/* Overview Tab */}
+
+
+
Description
+
{programme.description}
+
+
+
+
+
Programme Details
+
+
+ Start Date:
+ {new Date(programme.startDate).toLocaleDateString()}
+
+
+ End Date:
+ {new Date(programme.endDate).toLocaleDateString()}
+
+
+ Status:
+
+ {programme.status}
+
+
+
+
+
+
Instructor
+
+
+
+ {programme.instructor.name.split(' ').map(n => n[0]).join('')}
+
+
+
+
{programme.instructor.name}
+
{programme.instructor.email}
+
+
+
+
+
+
+
Quick Actions
+
+
+
+ Add Learners
+
+
+
+ Export Data
+
+
+
+ Analytics
+
+
+
+ Discussions
+
+
+
+
+
+ {/* Modules Tab */}
+
+ {programme.modules.map((module, index) => (
+
+
+
+
+ {getModuleIcon(module.type)}
+
+
+
+
+
+
{module.title}
+
+ {module.description}
+
+
+
+ {module.status}
+
+
+
+
+
+
+ {module.duration}
+
+
+ {getModuleIcon(module.type)}
+ {module.type.charAt(0).toUpperCase() + module.type.slice(1)}
+
+
+
+ {module.progress > 0 && (
+
+
+ Progress
+ {module.progress}%
+
+
+
+ )}
+
+
+
+
+ ))}
+
+
+ {/* Learners Tab */}
+
+
+
Enrolled Learners ({programme.enrolledCount})
+
+
+ Add Learners
+
+
+
+
+ {mockEnrolledLearners.map(learner => (
+
+
+
+
+
+
+ {learner.name.split(' ').map(n => n[0]).join('')}
+
+
+
+
{learner.name}
+
{learner.email}
+
+
+
+
+
+
{learner.progress}%
+
+
+
+ {learner.status}
+
+
+
+
+
+
+
+
+ ))}
+
+
+
+ {/* Resources Tab */}
+
+
+ {programme.resources.map(resource => (
+
+
+
+
+ {resource.type === 'pdf' && }
+ {resource.type === 'video' && }
+ {resource.type === 'link' && }
+ {resource.type === 'document' && }
+
+
+
+
{resource.title}
+
+ {resource.type.toUpperCase()} • {resource.size}
+
+
+
+
+
+
+
+
+
+ ))}
+
+
+
+
+
+
+ );
+};
+
+export default ProgrammeViewPage;
\ No newline at end of file
diff --git a/src/pages/ReportsPage/ReportsPage.tsx b/src/pages/ReportsPage/ReportsPage.tsx
new file mode 100644
index 0000000..25e2b93
--- /dev/null
+++ b/src/pages/ReportsPage/ReportsPage.tsx
@@ -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 (
+
+ {/* Header */}
+
+
+
Reports & Analytics
+
Track performance, engagement, and completion metrics
+
+
+
+
+
+
+
+ {programmes.map(p => (
+ {p.name}
+ ))}
+
+
+
+
+
+
+
+ Last 7 days
+ Last 30 days
+ Last 90 days
+ This Year
+
+
+ handleExport('excel')}
+ disabled={exporting}
+ className="min-tap-44"
+ >
+ {exporting ? (
+
+ ) : (
+
+ )}
+ Export
+
+
+
+
+ {/* Overview Stats */}
+
+
+
+
+
+
Total Learners
+
1,247
+
+12% from last month
+
+
+
+
+
+
+
+
+
+
Completion Rate
+
78%
+
+5% from last month
+
+
+
+
+
+
+
+
+
+
Avg. Time to Complete
+
21 days
+
+2 days from last month
+
+
+
+
+
+
+
+
+
+
Active Programmes
+
12
+
+3 from last month
+
+
+
+
+
+
+
+ {/* Main Tabs */}
+
+
+
+
+ Overview
+ Programmes
+ Learners
+ Engagement
+
+
+ {/* Overview Tab */}
+
+ {/* Completion Trends */}
+
+
+ Completion Trends
+ Monthly completion and enrollment rates
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* Programme Performance */}
+
+
+ Programme Performance
+ Completion rates by programme
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* Learner Status Distribution */}
+
+
+ Learner Status
+ Distribution by current status
+
+
+
+
+
+
+ {learnerStatusData.map((entry, index) => (
+ |
+ ))}
+
+
+
+
+
+
+
+
+
+
+
+ {/* Programmes Tab */}
+
+ {programmePerformanceData.map(programme => (
+
+
+
+
+
{programme.name}
+
+ {programme.enrollment} enrolled learners
+
+
+
+ {programme.completion}% Completion
+
+
+
+
+
+
+ Completion Progress
+ {programme.completion}%
+
+
+
+
+
+
+
Active
+
+ {Math.floor(programme.enrollment * 0.7)}
+
+
+
+
Completed
+
+ {Math.floor(programme.enrollment * 0.25)}
+
+
+
+
At Risk
+
+ {Math.floor(programme.enrollment * 0.05)}
+
+
+
+
+
+
+ ))}
+
+
+ {/* Learners Tab */}
+
+
+ {/* Top Performers */}
+
+
+ Top Performers
+ Learners with highest completion rates
+
+
+
+ {[1, 2, 3, 4, 5].map(i => (
+
+
+
+ JD
+
+
+
John Doe
+
Leadership Program
+
+
+
98%
+
+ ))}
+
+
+
+
+ {/* At Risk Learners */}
+
+
+ At Risk Learners
+ Learners needing intervention
+
+
+
+ {[1, 2, 3, 4, 5].map(i => (
+
+
+
+ JS
+
+
+
Jane Smith
+
Technical Skills
+
+
+
32%
+
+ ))}
+
+
+
+
+
+ {/* Learner Activity Table */}
+
+
+ Recent Learner Activity
+ Last 7 days of engagement
+
+
+
+
+
+
+ Learner
+ Programme
+ Progress
+ Last Activity
+ Status
+
+
+
+ {[1, 2, 3, 4, 5].map(i => (
+
+ Sarah Chen
+ Leadership
+
+
+
+ 2 hours ago
+
+ Active
+
+
+ ))}
+
+
+
+
+
+
+
+ {/* Engagement Tab */}
+
+ {/* Activity Heatmap */}
+
+
+ Weekly Activity Pattern
+ Learning activity by day and type
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* Engagement Metrics */}
+
+
+
+
+
85%
+
Video Completion Rate
+
+
+
+
+
+
+
4.2
+
Avg. Hours/Week
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* Export Options */}
+
+
+ Export Options
+ Download reports in various formats
+
+
+
+
handleExport('excel')}
+ >
+
+ Excel Report
+ .xlsx with charts
+
+
handleExport('pdf')}
+ >
+
+ PDF Report
+ Formatted summary
+
+
handleExport('csv')}
+ >
+
+ CSV Data
+ Raw data export
+
+
+
+
+
+ );
+};
+
+export default ReportsPage;
\ No newline at end of file
diff --git a/src/routes/index.tsx b/src/routes/index.tsx
new file mode 100644
index 0000000..399ee98
--- /dev/null
+++ b/src/routes/index.tsx
@@ -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: ,
+ },
+ {
+ path: '/hr',
+ element: ,
+ children: [
+ {
+ index: true,
+ element: ,
+ },
+ {
+ path: 'dashboard',
+ element: ,
+ },
+ {
+ path: 'learners',
+ element: ,
+ },
+ {
+ path: 'reports',
+ element: ,
+ },
+ {
+ path: 'discussions',
+ element: ,
+ },
+ {
+ path: 'programme/:programmeId',
+ element: ,
+ },
+ {
+ path: 'course/:courseId',
+ element: ,
+ },
+ {
+ path: 'profile',
+ element: , // You can create a separate ProfilePage later
+ },
+ ],
+ },
+]);
\ No newline at end of file
diff --git a/src/types/index.ts b/src/types/index.ts
new file mode 100644
index 0000000..557d60c
--- /dev/null
+++ b/src/types/index.ts
@@ -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;
+}
\ No newline at end of file
diff --git a/src/utils/mockData.ts b/src/utils/mockData.ts
new file mode 100644
index 0000000..82a13b4
--- /dev/null
+++ b/src/utils/mockData.ts
@@ -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' }
+ ]
+};
\ No newline at end of file