diff --git a/src/App.tsx b/src/App.tsx index ac5b9ee..7360e3d 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,5 +1,14 @@ 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'; @@ -14,7 +23,7 @@ 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 { Pagination, PaginationContent, PaginationItem, PaginationLink, PaginationNext, PaginationPrevious, PaginationEllipsis } from './components/ui/pagination'; import { Home, Users, @@ -52,8 +61,27 @@ import { CreditCard, Shield, ExternalLink, - Info + 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'; // Types interface KPIData { @@ -92,6 +120,48 @@ interface Deadline { 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; @@ -102,6 +172,21 @@ interface TestimonialFormData { 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(() => { @@ -182,6 +267,118 @@ const mockDeadlines: Deadline[] = [ { 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); @@ -221,14 +418,97 @@ const KPICard: React.FC<{ data: KPIData; onClick?: () => void; className?: strin ); }; +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} + +
+
+ +
+ + {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; -}> = ({ employees, onEdit, showProgress = true, maxHeight = "400px" }) => { + viewMode?: 'table' | 'cards'; +}> = ({ employees, onEdit, showProgress = true, maxHeight = "400px", viewMode = 'table' }) => { + if (viewMode === 'cards') { + return ( +
+ {employees.map((employee) => ( + + ))} +
+ ); + } + return ( -
+
@@ -336,9 +616,8 @@ const HRSidebar: React.FC<{ 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' } + { id: 'reports', label: 'Reports', icon: BarChart3, path: '/hr/reports' }, + { id: 'discussions', label: 'Discussion Forums', icon: MessageSquare, path: '/hr/discussions' } ]; return ( @@ -346,9 +625,9 @@ const HRSidebar: React.FC<{
- AC + TS
- Acme Corp + Tech Solutions Pvt Ltd
@@ -389,7 +668,9 @@ const HRSidebar: React.FC<{ const TopNav: React.FC<{ onMenuToggle?: () => void; showMenuButton?: boolean; -}> = ({ onMenuToggle, showMenuButton = false }) => { + onNotificationToggle?: () => void; + notificationCount?: number; +}> = ({ onMenuToggle, showMenuButton = false, onNotificationToggle, notificationCount = 0 }) => { return (
@@ -404,35 +685,130 @@ const TopNav: React.FC<{ )} -
- Logo +
+ Kautilya Leadership Centre
- -
- HR -
+ + {/* HR Profile Dropdown */} + + + + + + {/* User Profile Section */} +
+
+ P +
+
+

Priya

+

Tech Solutions Pvt Ltd

+
+
+ + {/* Switch Mode Section */} +
+

• Switch Mode

+
+ + +
+
+ + {/* Switch Accounts Section */} +
+

• Switch Accounts

+
+ + +
+
+ + + + {/* Profile & Settings */} + + +
+

Profile & Settings

+

Manage your account

+
+
+ + {/* Sign out */} + + +
+

Sign out

+

End your session

+
+
+
+
); }; -const BreadcrumbNav: React.FC<{ currentScreen: string }> = ({ currentScreen }) => { +const BreadcrumbNav: React.FC<{ currentScreen: string; currentProgrammeId?: string | null; currentCourseId?: string | null }> = ({ currentScreen, currentProgrammeId, currentCourseId }) => { const getBreadcrumbText = (screen: string) => { switch (screen) { - case 'home': return 'HR Home'; + case 'home': return 'Dashboard'; case 'learners': return 'Learners'; - case 'analytics': return 'Analytics & Reports'; - case 'settings': return 'HR Settings'; - case 'testimonials': return 'Testimonials'; + 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'; } }; @@ -452,11 +828,83 @@ const BreadcrumbNav: React.FC<{ currentScreen: string }> = ({ 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} + + +
+
+ ))} +
+ +
+ +
+
+ ); +}; + const ChatBot: React.FC<{ currentScreen?: string }> = ({ currentScreen }) => { const [isOpen, setIsOpen] = useState(false); const getChipsForScreen = (screen?: string) => { - if (screen === 'testimonials') { + if (screen === 'profile') { return [ "How do I submit a testimonial?", "When will my testimonial be reviewed?", @@ -475,7 +923,7 @@ const ChatBot: React.FC<{ currentScreen?: string }> = ({ currentScreen }) => { const chips = getChipsForScreen(currentScreen); return ( -
+
{isOpen && (
@@ -515,40 +963,19 @@ const ChatBot: React.FC<{ currentScreen?: string }> = ({ currentScreen }) => { }; // Screen Components -const HRHomeScreen: React.FC<{ onNavigate: (screen: string, filters?: any) => void }> = ({ onNavigate }) => { +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), 1000); + const timer = setTimeout(() => setLoading(false), 800); 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 (
@@ -581,188 +1008,24 @@ const HRHomeScreen: React.FC<{ onNavigate: (screen: string, filters?: any) => vo {/* Welcome Section */}
-

Hello HR Pooja 👋

-

See what's happening today at Acme Corp

+

Welcome Priya 👋

+

Manage programmes, track progress, and stay connected with your learning community

- {/* 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 Section - Unchanged */} + Quick Actions - Common HR tasks + Common HR tasks and shortcuts
{[ { 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') } + { 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 ( @@ -775,7 +1038,6 @@ const HRHomeScreen: React.FC<{ onNavigate: (screen: string, filters?: any) => vo ${prefersReducedMotion ? '' : 'animate-scale-hover'} `} aria-label={link.title} - aria-controls={link.title === 'Add Learners' ? 'learners-screen' : undefined} > {link.title} @@ -786,65 +1048,40 @@ const HRHomeScreen: React.FC<{ onNavigate: (screen: string, filters?: any) => vo - {/* Announcements & Reminders */} - - -
-
- Announcements & Reminders - Recent updates and notifications -
- -
-
- -
- {mockAnnouncements.map((item) => ( - - ))} -
-
-
+ {/* 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}`)} + /> +
); }; @@ -861,6 +1098,8 @@ const LearnersScreen: React.FC<{ filters?: any }> = ({ filters }) => { 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) => { @@ -880,6 +1119,17 @@ const LearnersScreen: React.FC<{ filters?: any }> = ({ filters }) => { 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]); @@ -890,12 +1140,17 @@ const LearnersScreen: React.FC<{ filters?: any }> = ({ filters }) => { const handleBulkSelect = (selectAll: boolean) => { if (selectAll) { - setSelectedEmployees(filteredEmployees.map(emp => emp.id)); + 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]); @@ -979,7 +1234,7 @@ const LearnersScreen: React.FC<{ filters?: any }> = ({ filters }) => {
- {selectedEmployees.length} learner{selectedEmployees.length !== 1 ? 's' : ''} selected + {selectedEmployees.length} learner{selectedEmployees.length !== 1 ? 's' : ''} selected {paginatedEmployees.length > 0 && selectedEmployees.length === paginatedEmployees.length ? '(current page)' : ''}
@@ -1044,16 +1317,15 @@ const LearnersScreen: React.FC<{ filters?: any }> = ({ filters }) => { - {filteredEmployees.map((employee) => ( + {paginatedEmployees.map((employee) => ( - handleEmployeeSelect(employee.id, e.target.checked)} + onCheckedChange={(checked) => handleEmployeeSelect(employee.id, !!checked)} className="min-tap-44" aria-label={`Select ${employee.name}`} /> @@ -1097,13 +1369,113 @@ const LearnersScreen: React.FC<{ filters?: any }> = ({ filters }) => {
+ + {/* 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 */} = ({ filters }) => { Add a new learner to the system. Email cannot be changed after saving. -
+
-
-
-