diff --git a/package-lock.json b/package-lock.json index 5bcb44c..a0e88c6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -34,6 +34,7 @@ "@radix-ui/react-toggle": "^1.1.2", "@radix-ui/react-toggle-group": "^1.1.2", "@radix-ui/react-tooltip": "^1.1.8", + "@reduxjs/toolkit": "^2.9.0", "@tailwindcss/postcss": "^4.1.12", "class-variance-authority": "^0.7.1", "clsx": "*", @@ -47,7 +48,9 @@ "react-day-picker": "^8.10.1", "react-dom": "^18.3.1", "react-hook-form": "^7.55.0", + "react-redux": "^9.2.0", "react-resizable-panels": "^2.1.7", + "react-router-dom": "^6.30.1", "recharts": "^2.15.2", "sonner": "^2.0.3", "tailwind-merge": "*", @@ -1889,6 +1892,41 @@ "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", "license": "MIT" }, + "node_modules/@reduxjs/toolkit": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.9.0.tgz", + "integrity": "sha512-fSfQlSRu9Z5yBkvsNhYF2rPS8cGXn/TZVrlwN1948QyZ8xMZ0JvP50S2acZNaf+o63u6aEeMjipFyksjIcWrog==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@standard-schema/utils": "^0.3.0", + "immer": "^10.0.3", + "redux": "^5.0.1", + "redux-thunk": "^3.1.0", + "reselect": "^5.1.0" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", + "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } + } + }, + "node_modules/@remix-run/router": { + "version": "1.23.0", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.0.tgz", + "integrity": "sha512-O3rHJzAQKamUz1fvE0Qaw0xSFqsA/yafi2iqeE0pvdFtCO1viYx8QL6f3Ln/aCCTLxs68SLf0KPM9eSeM8yBnA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/@rolldown/pluginutils": { "version": "1.0.0-beta.27", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", @@ -2176,6 +2214,18 @@ "win32" ] }, + "node_modules/@standard-schema/spec": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", + "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==", + "license": "MIT" + }, + "node_modules/@standard-schema/utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", + "license": "MIT" + }, "node_modules/@swc/core": { "version": "1.13.5", "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.13.5.tgz", @@ -2763,6 +2813,12 @@ "@types/react": "^19.0.0" } }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", + "license": "MIT" + }, "node_modules/@vitejs/plugin-react-swc": { "version": "3.11.0", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-3.11.0.tgz", @@ -3129,6 +3185,16 @@ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "license": "ISC" }, + "node_modules/immer": { + "version": "10.1.3", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.1.3.tgz", + "integrity": "sha512-tmjF/k8QDKydUlm3mZU+tjM6zeq9/fFpPqH9SzWmBnVVKsPBg/V66qsMwb3/Bo90cgUN+ghdVBess+hPsxUyRw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/input-otp": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/input-otp/-/input-otp-1.4.2.tgz", @@ -3612,6 +3678,29 @@ "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", "license": "MIT" }, + "node_modules/react-redux": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", + "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", + "license": "MIT", + "dependencies": { + "@types/use-sync-external-store": "^0.0.6", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "@types/react": "^18.2.25 || ^19", + "react": "^18.0 || ^19", + "redux": "^5.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "redux": { + "optional": true + } + } + }, "node_modules/react-remove-scroll": { "version": "2.7.1", "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.1.tgz", @@ -3669,6 +3758,38 @@ "react-dom": "^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, + "node_modules/react-router": { + "version": "6.30.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.1.tgz", + "integrity": "sha512-X1m21aEmxGXqENEPG3T6u0Th7g0aS4ZmoNynhbs+Cn+q+QGTLt+d5IQ2bHAXKzKcxGJjxACpVbnYQSCRcfxHlQ==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.23.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.30.1", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.1.tgz", + "integrity": "sha512-llKsgOkZdbPU1Eg3zK8lCn+sjD9wMRZZPuzmdWWX5SUs8OFkN5HnFVC0u5KMeMaC9aoancFI/KoLuKPqN+hxHw==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.23.0", + "react-router": "6.30.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, "node_modules/react-smooth": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.4.tgz", @@ -3754,6 +3875,27 @@ "decimal.js-light": "^2.4.1" } }, + "node_modules/redux": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", + "license": "MIT" + }, + "node_modules/redux-thunk": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", + "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", + "license": "MIT", + "peerDependencies": { + "redux": "^5.0.0" + } + }, + "node_modules/reselect": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", + "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==", + "license": "MIT" + }, "node_modules/rollup": { "version": "4.49.0", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.49.0.tgz", diff --git a/package.json b/package.json index 909892d..bcf4b48 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "@radix-ui/react-toggle": "^1.1.2", "@radix-ui/react-toggle-group": "^1.1.2", "@radix-ui/react-tooltip": "^1.1.8", + "@reduxjs/toolkit": "^2.9.0", "@tailwindcss/postcss": "^4.1.12", "class-variance-authority": "^0.7.1", "clsx": "*", @@ -42,7 +43,9 @@ "react-day-picker": "^8.10.1", "react-dom": "^18.3.1", "react-hook-form": "^7.55.0", + "react-redux": "^9.2.0", "react-resizable-panels": "^2.1.7", + "react-router-dom": "^6.30.1", "recharts": "^2.15.2", "sonner": "^2.0.3", "tailwind-merge": "*", diff --git a/src/App.css b/src/App.css new file mode 100644 index 0000000..0cf865b --- /dev/null +++ b/src/App.css @@ -0,0 +1,73 @@ +/* Add this to your main global CSS file */ +@media (max-width: 640px) { + .employee-card-mobile-view { + display: block; + } + + .employee-card-desktop-view { + display: none; + } +} + +@media (min-width: 641px) { + .employee-card-mobile-view { + display: none; + } + + .employee-card-desktop-view { + display: block; + } +} + +/* Custom styles for the compact table */ +.compact-table { + font-size: 0.875rem; +} + +.compact-table th, +.compact-table td { + padding: 0.5rem 0.75rem; +} + +/* Responsive table container */ +.table-container { + overflow-x: auto; + max-width: 100%; +} + +/* Ensure buttons are touch-friendly on mobile */ +.min-tap-44 { + min-height: 44px; + min-width: 44px; +} + +/* Truncate text for small containers */ +.truncate { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +/* Animation for card entrance */ +@keyframes slideUp { + from { + opacity: 0; + transform: translateY(20px); + } + + to { + opacity: 1; + transform: translateY(0); + } +} + +.animate-slide-up { + animation: slideUp 0.5s ease-out forwards; +} + +/* Reduced motion support */ +@media (prefers-reduced-motion: reduce) { + .animate-slide-up { + animation: none; + } +} \ No newline at end of file diff --git a/src/App.tsx b/src/App.tsx index ac5b9ee..157d55b 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,2142 +1,32 @@ -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 logo from '../src/assets/klc-logo.png'; -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 EmployeeTable: React.FC<{ - employees: Employee[]; - onEdit?: (employee: Employee) => void; - showProgress?: boolean; - maxHeight?: string; -}> = ({ employees, onEdit, showProgress = true, maxHeight = "400px" }) => { - 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} - - )} - - - -
- ))} -
-
-
- ); -}; - -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: 'analytics', label: 'Analytics', icon: BarChart3, path: '/hr/analytics' }, - { id: 'testimonials', label: 'Testimonials', icon: MessageSquare, path: '/hr/testimonials' }, - { id: 'settings', label: 'Settings', icon: Settings, path: '/hr/settings' } - ]; - - return ( -
-
-
-
- AC -
- Acme Corp -
-
- - -
- ); -}; - -const TopNav: React.FC<{ - onMenuToggle?: () => void; - showMenuButton?: boolean; -}> = ({ onMenuToggle, showMenuButton = false }) => { - return ( -
-
- {showMenuButton && ( - - )} -
- Logo -
-
- -
- -
- HR -
-
-
- ); -}; - -const BreadcrumbNav: React.FC<{ currentScreen: string }> = ({ currentScreen }) => { - const getBreadcrumbText = (screen: string) => { - switch (screen) { - case 'home': return 'HR Home'; - case 'learners': return 'Learners'; - case 'analytics': return 'Analytics & Reports'; - case 'settings': return 'HR Settings'; - case 'testimonials': return 'Testimonials'; - default: return 'HR Portal'; - } - }; - - return ( - - - - HR Portal - - - - ); -}; - -const ChatBot: React.FC<{ currentScreen?: string }> = ({ currentScreen }) => { - const [isOpen, setIsOpen] = useState(false); - - const getChipsForScreen = (screen?: string) => { - if (screen === 'testimonials') { - 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

- -
-
- {chips.map((chip, index) => ( - - ))} -
-
- )} - -
- ); -}; - -// 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), 1000); - return () => clearTimeout(timer); - }, []); - - const handleKPIClick = (kpiTitle: string) => { - let filters = {}; - switch (kpiTitle) { - case 'Total Learners': - filters = { status: 'all' }; - break; - case 'Active Courses': - filters = { status: 'active' }; - break; - case 'Completed Profilers': - filters = { completed: true }; - break; - default: - filters = {}; - } - onNavigate('learners', filters); - }; - - const cohortData = [ - { name: 'Leadership Development', notStarted: 15, inProgress: 28, completed: 42 }, - { name: 'Technical Skills', notStarted: 22, inProgress: 35, completed: 38 }, - { name: 'Communication', notStarted: 18, inProgress: 24, completed: 31 }, - { name: 'Project Management', notStarted: 12, inProgress: 19, completed: 28 } - ]; - - if (loading) { - return ( -
-
- {[...Array(4)].map((_, i) => ( - - - - - - - - - ))} -
- - - - - - - - -
- ); - } - - return ( -
- {/* Welcome Section */} -
-
-

Hello HR Pooja 👋

-

See what's happening today at Acme Corp

-
-
- - {/* KPI Cards */} -
- {mockKPIData.map((kpi, index) => ( - handleKPIClick(kpi.title)} - className={prefersReducedMotion ? '' : 'animate-fade-in'} - style={{ animationDelay: prefersReducedMotion ? '0ms' : `${index * 100 + 200}ms` }} - /> - ))} -
- - {/* Employee Assignment & Progress */} - - -
-
- Employee Assignment & Progress - Snapshot of current learning activities -
-
- - - -
-
-
- - onNavigate('learners', { editEmployee: employee.id })} - maxHeight="360px" - /> - -
- -
- {/* Cohort Progress Chart */} - - -
-
- Cohort Progress - Progress overview by programme -
- - - Auto-refresh - -
-
- -
-
- Stacked bar chart showing progress across different learning programmes. - Each bar represents not started, in progress, and completed learners. -
- {cohortData.map((cohort, index) => { - const total = cohort.notStarted + cohort.inProgress + cohort.completed; - const notStartedPercent = (cohort.notStarted / total) * 100; - const inProgressPercent = (cohort.inProgress / total) * 100; - const completedPercent = (cohort.completed / total) * 100; - - return ( -
-
- {cohort.name} - {total} learners -
-
-
-
-
-
-
- Not Started: {cohort.notStarted} - In Progress: {cohort.inProgress} - Completed: {cohort.completed} -
-
- ); - })} -
- - - - {/* Upcoming Deadlines */} - - - Upcoming Deadlines - Next 7 days - - -
- {mockDeadlines.map((deadline) => ( -
-
-
- {deadline.type === 'webinar' ? - : - - } -
-
-

{deadline.title}

-

{deadline.type}

-
-
-
- - {deadline.dueDate} - -

{deadline.dueTime}

-
-
- ))} -
-
-
-
- - {/* Quick Links */} - - - Quick Actions - Common HR tasks - - -
- {[ - { title: 'Add Learners', icon: Plus, action: () => onNavigate('learners', { action: 'add' }) }, - { title: 'Assign Courses', icon: BookOpen, action: () => onNavigate('learners', { action: 'assign' }) }, - { title: 'Download Reports', icon: Download, action: () => onNavigate('analytics') }, - { title: 'Testimonials Queue', icon: MessageSquare, action: () => onNavigate('testimonials') } - ].map((link, index) => { - const Icon = link.icon; - return ( - - ); - })} -
-
-
- - {/* Announcements & Reminders */} - - -
-
- Announcements & Reminders - Recent updates and notifications -
- -
-
- -
- {mockAnnouncements.map((item) => ( - - ))} -
-
-
-
- ); -}; - -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" - /> -
- -
-
- - -
-
-
-
- - {/* Bulk Action Bar */} - {bulkActionVisible && ( - - -
- - {selectedEmployees.length} learner{selectedEmployees.length !== 1 ? 's' : ''} selected - -
- - - -
-
-
-
- )} - - {/* Learners Table */} - - -
-
- Learners ({filteredEmployees.length}) - Manage learner accounts and assignments -
-
- -
-
-
- -
- - - - Select - Name - Email - Phone - Status - Actions - - - - {filteredEmployees.map((employee) => ( - - - handleEmployeeSelect(employee.id, e.target.checked)} - className="min-tap-44" - aria-label={`Select ${employee.name}`} - /> - - {employee.name} - {employee.email} - {employee.phone} - - - {employee.status} - - - -
- - -
-
-
- ))} -
-
-
-
-
- - {/* Add Learner Drawer */} - - - - Add New Learner - - Add a new learner to the system. Email cannot be changed after saving. - - -
-
- - setNewEmployee(prev => ({ ...prev, name: e.target.value }))} - placeholder="Enter full name" - required - aria-required="true" - /> -
-
- - setNewEmployee(prev => ({ ...prev, email: e.target.value }))} - placeholder="email@company.com" - required - aria-required="true" - /> -
-
- - setNewEmployee(prev => ({ ...prev, phone: e.target.value }))} - placeholder="+61 4XX XXX XXX" - /> -
-
- - -
-
-
-
- - {/* 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. -

- -
-
-

Step 2: Upload File

-
- -

- Drag and drop your CSV file here, or click to browse -

- -
-
-
- - -
-
-
-
- - {/* Assign Modal */} - - - - Assign to Programme/Course - - Assign {selectedEmployees.length} selected learner{selectedEmployees.length !== 1 ? 's' : ''} to a programme or course. - - -
-
- - -
-
- - -
-
- - -
-
-
-
- - {/* Edit/Assign Drawer */} - - - - - {editingEmployee?.name} - - - Edit learner details and manage course assignments. - - - {editingEmployee && ( - - - Details - Enrolments - - -
- - -
-
- - -
-
- - -
-
- -
-
-

Current Enrolments

- -
- {editingEmployee.programme && ( - - -
-
-

{editingEmployee.programme}

-

{editingEmployee.course}

- {editingEmployee.progress !== undefined && ( - - )} -
- -
-
-
- )} -
-
-
- - -
-
- )} -
-
-
- ); -}; - -const AnalyticsScreen: React.FC<{ filters?: any }> = ({ filters }) => { - const [dateRange, setDateRange] = useState('last-30-days'); - const [selectedProgrammes, setSelectedProgrammes] = useState(['all']); - const [loading, setLoading] = useState(false); - const [exporting, setExporting] = useState(false); - - const analyticsKPIData: KPIData[] = [ - { title: 'Total Learners', value: 1247, change: 8.2, trend: 'up' }, - { title: 'New Enrolments', value: 89, change: 15.3, trend: 'up' }, - { title: 'Course Completions', value: 342, change: -2.1, trend: 'down' }, - { title: 'Assessment Rates', value: 78, change: 5.7, trend: 'up' } - ]; - - const handleExport = async (format: 'csv' | 'pdf') => { - setExporting(true); - // Simulate export process - await new Promise(resolve => setTimeout(resolve, 2000)); - setExporting(false); - console.log(`Exported as ${format.toUpperCase()}`); - }; - - const handleRunReport = () => { - setLoading(true); - // Simulate report generation - setTimeout(() => setLoading(false), 1500); - }; - - const chartData = [ - { month: 'Jan', enrolments: 45, completions: 38, assessments: 42 }, - { month: 'Feb', enrolments: 52, completions: 41, assessments: 38 }, - { month: 'Mar', enrolments: 48, completions: 44, assessments: 46 }, - { month: 'Apr', enrolments: 61, completions: 49, assessments: 52 }, - { month: 'May', enrolments: 55, completions: 52, assessments: 48 }, - { month: 'Jun', enrolments: 67, completions: 58, assessments: 61 } - ]; - - return ( -
- {/* Filter Bar */} - - -
-
-
- - -
-
- - -
-
- -
-
-
- - {/* KPI Cards */} -
- {analyticsKPIData.map((kpi, index) => ( - - ))} -
- - {/* Charts Panel */} - - -
-
- Learning Analytics Overview - Key metrics over time -
-
- Last refreshed: 10 minutes ago -
-
-
- -
-
- -

Interactive chart would be rendered here

-

- Line/Bar chart showing enrolments, completions, and assessments over time -

-
-
-
- Line and bar chart showing learning analytics over the selected time period. - Displays new enrolments, course completions, and assessment completion rates. - Chart includes interactive legend for toggling data series visibility. -
-
-
- - {/* Detailed Table */} - - -
-
- Assignments & Progress Detail - Complete learner progress breakdown -
-
- - -
-
-
- - - -
- - {/* Data Freshness Note */} -
- - Last refreshed: {new Date().toLocaleTimeString()} • - Next refresh in 4 minutes - -
-
- ); -}; - -const TestimonialsScreen: 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. - - - - )} - - {/* 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 */} -
- - -

- Pre-filled from your profile -

-
- - {/* Work Email */} -
- - -

- Pre-filled from your profile -

-
- - {/* Phone */} -
- - handleInputChange('phone', e.target.value)} - placeholder="+61 4XX XXX XXX" - className="min-tap-44" - /> -
- - {/* Organisation */} -
- - -

- Pre-filled from your profile -

-
-
- - {/* Programme */} -
- - -
- - {/* Testimonial Text */} -
- -