all src changes with new one

This commit is contained in:
priyanshuvish
2025-09-26 16:37:17 +05:30
parent 7f14ac6a59
commit 9823bf9a9e
17 changed files with 12701 additions and 542 deletions

File diff suppressed because it is too large Load Diff

2343
src/App_new.tsx Normal file

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View File

@@ -0,0 +1,379 @@
import React, { useState } from 'react';
import { Button } from './ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from './ui/card';
import { Badge } from './ui/badge';
import { Input } from './ui/input';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './ui/select';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from './ui/table';
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from './ui/dialog';
import {
Search,
Filter,
Download,
Users,
Calendar,
BookOpen,
RefreshCw,
Eye,
UserPlus,
ChevronLeft,
ChevronRight
} from 'lucide-react';
interface Assignment {
startDate: Date;
endDate: Date;
}
interface Programme {
programmeId: string;
title: string;
status: 'Active' | 'Upcoming' | 'Completed';
coursesCount: number;
contentCount: number;
assignment: Assignment;
learnersAssigned: number;
}
interface ProgrammesTableProps {
programmes?: Programme[];
onViewProgramme?: (programmeId: string) => void;
onAssignLearners?: (programmeId: string) => void;
onDownloadTracker?: (programmeId: string) => void;
}
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
},
{
programmeId: 'prog-006',
title: 'Data Analytics Fundamentals',
status: 'Completed',
coursesCount: 7,
contentCount: 21,
assignment: {
startDate: new Date('2023-10-15'),
endDate: new Date('2024-01-15')
},
learnersAssigned: 29
}
];
export const ProgrammesTable: React.FC<ProgrammesTableProps> = ({
programmes = mockProgrammes,
onViewProgramme,
onAssignLearners,
onDownloadTracker
}) => {
const [searchTerm, setSearchTerm] = useState('');
const [statusFilter, setStatusFilter] = useState('Active');
const [currentPage, setCurrentPage] = useState(1);
const [isAssignModalOpen, setIsAssignModalOpen] = useState(false);
const [selectedProgramme, setSelectedProgramme] = useState<Programme | null>(null);
const [isExporting, setIsExporting] = useState(false);
const itemsPerPage = 5;
const filteredProgrammes = programmes.filter(prog => {
const matchesSearch = prog.title.toLowerCase().includes(searchTerm.toLowerCase());
const matchesStatus = statusFilter === 'all' || prog.status === statusFilter;
return matchesSearch && matchesStatus;
});
// Pagination logic
const totalPages = Math.ceil(filteredProgrammes.length / itemsPerPage);
const startIndex = (currentPage - 1) * itemsPerPage;
const endIndex = startIndex + itemsPerPage;
const paginatedProgrammes = filteredProgrammes.slice(startIndex, endIndex);
// Reset to first page when filters change
React.useEffect(() => {
setCurrentPage(1);
}, [searchTerm, statusFilter]);
const formatDateRange = (assignment: Assignment) => {
const startDate = assignment.startDate.toLocaleDateString('en-AU', {
day: '2-digit',
month: 'short',
year: 'numeric'
});
const endDate = assignment.endDate.toLocaleDateString('en-AU', {
day: '2-digit',
month: 'short',
year: 'numeric'
});
return `${startDate}${endDate}`;
};
const handleViewProgramme = (programmeId: string) => {
onViewProgramme?.(programmeId);
console.log(`Viewing programme: ${programmeId}`);
};
const handleAssignLearners = (programme: Programme) => {
setSelectedProgramme(programme);
setIsAssignModalOpen(true);
onAssignLearners?.(programme.programmeId);
};
const handleDownloadTracker = async (programmeId: string) => {
setIsExporting(true);
await new Promise(resolve => setTimeout(resolve, 2000));
setIsExporting(false);
onDownloadTracker?.(programmeId);
console.log(`Downloaded tracker for programme: ${programmeId}`);
};
const getStatusBadgeProps = (status: Programme['status']) => {
switch (status) {
case 'Active':
return { variant: 'default' as const, className: 'bg-status-success text-status-success-foreground' };
case 'Upcoming':
return { variant: 'secondary' as const, className: 'bg-status-warn text-status-warn-foreground' };
case 'Completed':
return { variant: 'outline' as const, className: 'bg-muted text-muted-foreground' };
default:
return { variant: 'secondary' as const };
}
};
return (
<Card>
<CardHeader>
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div>
<CardTitle>Programmes</CardTitle>
<CardDescription>Manage programme assignments and track progress</CardDescription>
</div>
<div className="flex items-center gap-2">
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
placeholder="Search programmes..."
className="pl-10 w-[200px]"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
aria-label="Search programmes by title"
/>
</div>
<Select value={statusFilter} onValueChange={setStatusFilter}>
<SelectTrigger className="w-[140px]">
<Filter className="h-4 w-4 mr-2" />
<SelectValue placeholder="Status" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Status</SelectItem>
<SelectItem value="Active">Active</SelectItem>
<SelectItem value="Upcoming">Upcoming</SelectItem>
<SelectItem value="Completed">Completed</SelectItem>
</SelectContent>
</Select>
</div>
</div>
</CardHeader>
<CardContent>
<div className="rounded-md border" style={{ maxWidth: '1200px' }}>
<Table>
<TableHeader className="sticky-header">
<TableRow>
<TableHead className="w-[300px]">Programme Title</TableHead>
<TableHead className="w-[150px]">Courses / Content</TableHead>
<TableHead className="w-[200px]">Start End</TableHead>
<TableHead className="w-[120px]">Learners Assigned</TableHead>
<TableHead className="w-[200px]">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{paginatedProgrammes.map((programme) => (
<TableRow key={programme.programmeId} className="min-h-[44px]">
<TableCell>
<div className="flex items-center gap-2">
<span className="font-medium">{programme.title}</span>
<Badge
{...getStatusBadgeProps(programme.status)}
aria-label={`Programme status: ${programme.status}`}
>
{programme.status}
</Badge>
</div>
</TableCell>
<TableCell>
<div className="flex items-center gap-1 text-sm text-muted-foreground">
<BookOpen className="h-4 w-4" />
<span>{programme.coursesCount} {programme.contentCount}</span>
</div>
</TableCell>
<TableCell className="text-sm">
{formatDateRange(programme.assignment)}
</TableCell>
<TableCell>
<div className="flex items-center gap-1">
<Users className="h-4 w-4 text-muted-foreground" />
<span className="font-medium">{programme.learnersAssigned}</span>
</div>
</TableCell>
<TableCell>
<div className="flex items-center gap-1">
<Button
variant="ghost"
size="sm"
onClick={() => handleViewProgramme(programme.programmeId)}
className="min-tap-44"
aria-label={`View programme details for ${programme.title}`}
>
<Eye className="h-4 w-4" />
<span className="sr-only">View Programme</span>
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => handleAssignLearners(programme)}
className="min-tap-44"
aria-label={`Assign learners to ${programme.title}`}
>
<UserPlus className="h-4 w-4" />
<span className="sr-only">Assign Learners</span>
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => handleDownloadTracker(programme.programmeId)}
disabled={isExporting}
className="min-tap-44"
aria-label={`Download tracker for ${programme.title}`}
>
{isExporting ? (
<RefreshCw className="h-4 w-4 animate-spin" />
) : (
<Download className="h-4 w-4" />
)}
<span className="sr-only">Download Tracker</span>
</Button>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
{/* Pagination */}
{totalPages > 1 && (
<div className="flex items-center justify-between mt-4">
<div className="text-sm text-muted-foreground">
Showing {startIndex + 1} to {Math.min(endIndex, filteredProgrammes.length)} of {filteredProgrammes.length} programmes
</div>
<div className="flex items-center gap-2">
<Button
variant="outline"
size="sm"
onClick={() => setCurrentPage(prev => Math.max(1, prev - 1))}
disabled={currentPage === 1}
className="min-tap-44"
>
<ChevronLeft className="h-4 w-4 mr-1" />
Previous
</Button>
<span className="text-sm text-muted-foreground">
Page {currentPage} of {totalPages}
</span>
<Button
variant="outline"
size="sm"
onClick={() => setCurrentPage(prev => Math.min(totalPages, prev + 1))}
disabled={currentPage === totalPages}
className="min-tap-44"
>
Next
<ChevronRight className="h-4 w-4 ml-1" />
</Button>
</div>
</div>
)}
{filteredProgrammes.length === 0 && (
<div className="text-center py-8">
<p className="text-muted-foreground">No programmes found matching your criteria.</p>
</div>
)}
{/* Assignment Modal */}
<Dialog open={isAssignModalOpen} onOpenChange={setIsAssignModalOpen}>
<DialogContent className="sm:max-w-[600px]" role="dialog" aria-modal="true">
<DialogHeader>
<DialogTitle>Assign Learners to Programme</DialogTitle>
<DialogDescription>
{selectedProgramme && `Assign learners to "${selectedProgramme.title}"`}
</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-4">
<div className="text-center py-8 text-muted-foreground">
<UserPlus className="h-12 w-12 mx-auto mb-4 text-muted-foreground" />
<p>Assignment wizard would be displayed here</p>
<p className="text-sm mt-2">Including org/individual selection, dates, HR contacts, and participant upload</p>
</div>
</div>
</DialogContent>
</Dialog>
</CardContent>
</Card>
);
};

View File

@@ -0,0 +1,839 @@
import React, { useState, useEffect } from 'react';
import { Button } from './ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from './ui/card';
import { Badge } from './ui/badge';
import { Input } from './ui/input';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './ui/select';
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from './ui/dialog';
import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle, SheetTrigger } from './ui/sheet';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from './ui/table';
import { Progress } from './ui/progress';
import { Tabs, TabsContent, TabsList, TabsTrigger } from './ui/tabs';
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from './ui/accordion';
import {
ArrowLeft,
Download,
BarChart3,
MoreHorizontal,
Users,
Clock,
BookOpen,
Award,
Search,
Eye,
Mail,
FileText,
Play,
RefreshCw,
ExternalLink,
CheckCircle,
AlertCircle,
XCircle,
Video,
FileQuestion,
Activity,
Building2
} from 'lucide-react';
// Types
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[];
}
interface CourseModule {
id: string;
title: string;
lessons: CourseLesson[];
}
interface CourseLesson {
id: string;
title: string;
type: 'video' | 'quiz' | 'read' | 'assignment';
eta: string;
dueDate?: string;
status?: 'Not Started' | 'In Progress' | 'Completed';
}
interface LinkedProgramme {
id: string;
title: string;
}
interface CourseAssignment {
startDate: string;
endDate: string;
orgId: string;
orgName: string;
}
interface CourseCounts {
learners: number;
avgProgress: number;
modules: number;
lessons: number;
}
interface CourseLearner {
id: string;
name: string;
email: string;
progressPct: number;
currentLesson: {
id: string;
title: string;
status: 'Not Started' | 'In-Progress' | 'Completed';
};
lastActivity: string;
attempts?: number;
avgScore?: number;
}
interface CourseHRViewProps {
courseId: string;
onBack: () => void;
onAssignLearners: (courseId: string) => void;
onDownloadTracker: (courseId: string) => void;
onOpenAnalytics: (courseId: string) => void;
}
// Mock data
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' }
]
};
const mockAssignment: CourseAssignment = {
startDate: '2024-01-01',
endDate: '2024-02-15',
orgId: 'org_123',
orgName: 'Tech Solutions Pvt Ltd'
};
const mockCounts: CourseCounts = {
learners: 15,
avgProgress: 58,
modules: 3,
lessons: 9
};
const mockLearners: CourseLearner[] = [
{
id: 'l1',
name: 'Sarah Chen',
email: 'sarah.chen@company.com',
progressPct: 75,
currentLesson: { id: 'l3', title: 'Knowledge Check', status: 'In-Progress' },
lastActivity: '2 hours ago',
attempts: 2,
avgScore: 87
},
{
id: 'l2',
name: 'Michael Rodriguez',
email: 'michael.r@company.com',
progressPct: 45,
currentLesson: { id: 'l2', title: 'Strategic Frameworks Overview', status: 'In-Progress' },
lastActivity: '1 day ago',
attempts: 1,
avgScore: 92
},
{
id: 'l3',
name: 'Emma Thompson',
email: 'emma.thompson@company.com',
progressPct: 89,
currentLesson: { id: 'l6', title: 'Case Study Analysis', status: 'In-Progress' },
lastActivity: '3 hours ago',
attempts: 3,
avgScore: 94
},
{
id: 'l4',
name: 'David Kim',
email: 'david.kim@company.com',
progressPct: 23,
currentLesson: { id: 'l1', title: 'Introduction to Strategic Thinking', status: 'In-Progress' },
lastActivity: '5 hours ago',
attempts: 1,
avgScore: 78
}
];
export const CourseHRView: React.FC<CourseHRViewProps> = ({
courseId,
onBack,
onAssignLearners,
onDownloadTracker,
onOpenAnalytics
}) => {
const [activeTab, setActiveTab] = useState('overview');
const [loading, setLoading] = useState(true);
const [searchTerm, setSearchTerm] = useState('');
const [statusFilter, setStatusFilter] = useState('all');
const [selectedLearner, setSelectedLearner] = useState<CourseLearner | null>(null);
const [showLearnerDrawer, setShowLearnerDrawer] = useState(false);
const [exporting, setExporting] = useState(false);
// Simulate loading
useEffect(() => {
const timer = setTimeout(() => setLoading(false), 800);
return () => clearTimeout(timer);
}, []);
const getTypeIcon = (type: string) => {
switch (type) {
case 'video': return <Video className="h-4 w-4" />;
case 'quiz': return <FileQuestion className="h-4 w-4" />;
case 'read': return <FileText className="h-4 w-4" />;
case 'assignment': return <Activity className="h-4 w-4" />;
default: return <BookOpen className="h-4 w-4" />;
}
};
const getStatusBadge = (status?: string) => {
switch (status) {
case 'Completed':
return <Badge variant="default" className="bg-status-success text-white"><CheckCircle className="h-3 w-3 mr-1" />Completed</Badge>;
case 'In Progress':
return <Badge variant="secondary" className="bg-status-warn text-black"><AlertCircle className="h-3 w-3 mr-1" />In Progress</Badge>;
case 'Not Started':
return <Badge variant="outline"><XCircle className="h-3 w-3 mr-1" />Not Started</Badge>;
default:
return null;
}
};
const filteredLearners = mockLearners.filter(learner => {
const matchesSearch = learner.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
learner.email.toLowerCase().includes(searchTerm.toLowerCase());
const matchesStatus = statusFilter === 'all' || learner.currentLesson.status === statusFilter;
return matchesSearch && matchesStatus;
});
const handleViewLearner = (learner: CourseLearner) => {
setSelectedLearner(learner);
setShowLearnerDrawer(true);
};
const handleExport = async (format: 'excel' | 'csv' | 'pdf') => {
setExporting(true);
await new Promise(resolve => setTimeout(resolve, 2000));
setExporting(false);
console.log(`Exported course tracker as ${format.toUpperCase()}`);
};
if (loading) {
return (
<div className="space-y-6 animate-pulse">
<div className="h-16 bg-muted rounded-lg"></div>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
{[...Array(4)].map((_, i) => (
<div key={i} className="h-24 bg-muted rounded-lg"></div>
))}
</div>
<div className="h-96 bg-muted rounded-lg"></div>
</div>
);
}
return (
<div className="space-y-6">
{/* Header */}
<Card className="sticky top-0 z-20 bg-background border-b shadow-sm">
<CardContent className="pt-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<Button
variant="ghost"
size="icon"
onClick={onBack}
className="min-tap-44"
aria-label="Go back to courses list"
>
<ArrowLeft className="h-4 w-4" />
</Button>
<div>
<div className="flex items-center gap-3 mb-1">
<h1 className="text-2xl font-bold">{mockCourse.title}</h1>
<Badge
variant={mockCourse.status === 'Published' ? 'default' : 'secondary'}
className={mockCourse.status === 'Published' ? 'bg-status-success' : ''}
>
{mockCourse.status}
</Badge>
</div>
<p className="text-muted-foreground">
{mockCourse.code} {mockCourse.owner} Version {mockCourse.version} Duration: {mockCourse.duration}
</p>
</div>
</div>
<div className="flex items-center gap-2">
<Button
onClick={() => onAssignLearners(courseId)}
className="min-tap-44"
>
<Users className="h-4 w-4 mr-2" />
Assign Learners
</Button>
<Button
variant="outline"
onClick={() => onDownloadTracker(courseId)}
disabled={exporting}
className="min-tap-44"
>
{exporting ? (
<RefreshCw className="h-4 w-4 mr-2 animate-spin" />
) : (
<Download className="h-4 w-4 mr-2" />
)}
Download Tracker
</Button>
<Button
variant="outline"
onClick={() => onOpenAnalytics(courseId)}
className="min-tap-44"
>
<BarChart3 className="h-4 w-4 mr-2" />
Open Analytics
</Button>
<Dialog>
<DialogTrigger asChild>
<Button
variant="ghost"
size="icon"
className="min-tap-44"
aria-label="More actions"
>
<MoreHorizontal className="h-4 w-4" />
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle>Course Actions</DialogTitle>
<DialogDescription>Additional actions for this course</DialogDescription>
</DialogHeader>
<div className="space-y-2">
<Button variant="outline" className="w-full justify-start">
<FileText className="h-4 w-4 mr-2" />
View Syllabus
</Button>
<Button variant="outline" className="w-full justify-start">
<ExternalLink className="h-4 w-4 mr-2" />
Audit Trail
</Button>
</div>
</DialogContent>
</Dialog>
</div>
</div>
</CardContent>
</Card>
{/* Summary Cards */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
<Card>
<CardContent className="pt-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-muted-foreground">Assignment Window</p>
<p className="font-semibold">{new Date(mockAssignment.startDate).toLocaleDateString()} {new Date(mockAssignment.endDate).toLocaleDateString()}</p>
<p className="text-xs text-muted-foreground">{mockAssignment.orgName}</p>
</div>
<Clock className="h-8 w-8 text-brand-primary" />
</div>
</CardContent>
</Card>
<Card>
<CardContent className="pt-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-muted-foreground">Enrolled Learners</p>
<p className="text-2xl font-bold">{mockCounts.learners}</p>
<Button variant="link" className="p-0 h-auto text-xs">Manage</Button>
</div>
<Users className="h-8 w-8 text-brand-primary" />
</div>
</CardContent>
</Card>
<Card>
<CardContent className="pt-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-muted-foreground">Avg Progress</p>
<p className="text-2xl font-bold">{mockCounts.avgProgress}%</p>
<Progress value={mockCounts.avgProgress} className="w-16 mt-1" />
</div>
<Award className="h-8 w-8 text-brand-primary" />
</div>
</CardContent>
</Card>
<Card>
<CardContent className="pt-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-muted-foreground">Course Structure</p>
<div className="flex gap-2 text-sm">
<span>{mockCounts.modules} Modules</span>
<span></span>
<span>{mockCounts.lessons} Lessons</span>
</div>
<p className="text-xs text-muted-foreground">Duration: {mockCourse.duration}</p>
</div>
<BookOpen className="h-8 w-8 text-brand-primary" />
</div>
</CardContent>
</Card>
</div>
{/* Tabs */}
<Card>
<CardContent className="pt-6">
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
<TabsList className="grid w-full grid-cols-5">
<TabsTrigger value="overview">Overview</TabsTrigger>
<TabsTrigger value="syllabus">Syllabus</TabsTrigger>
<TabsTrigger value="learners">Learners</TabsTrigger>
<TabsTrigger value="reports">Reports</TabsTrigger>
<TabsTrigger value="activity">Activity</TabsTrigger>
</TabsList>
{/* Overview Tab */}
<TabsContent value="overview" className="space-y-6 mt-6">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<Card>
<CardHeader>
<CardTitle>Course Summary</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div>
<h4 className="font-medium mb-2">Description</h4>
<p className="text-sm text-muted-foreground">{mockCourse.description}</p>
</div>
<div>
<h4 className="font-medium mb-2">Learning Objectives</h4>
<ul className="text-sm text-muted-foreground space-y-1">
{mockCourse.objectives.map((objective, index) => (
<li key={index} className="flex items-start gap-2">
<CheckCircle className="h-4 w-4 text-status-success mt-0.5 flex-shrink-0" />
{objective}
</li>
))}
</ul>
</div>
<div>
<h4 className="font-medium mb-2">Tags</h4>
<div className="flex flex-wrap gap-2">
{mockCourse.tags.map((tag, index) => (
<Badge key={index} variant="outline">{tag}</Badge>
))}
</div>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Linked Resources</CardTitle>
<CardDescription>Read-only view of course resources</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div>
<h4 className="font-medium mb-2">Associated Programmes</h4>
{mockCourse.linkedProgrammes.length > 0 ? (
<div className="space-y-2">
<p className="text-sm text-muted-foreground mb-2">
Appears in {mockCourse.linkedProgrammes.length} programmes:
</p>
<div className="space-y-1">
{mockCourse.linkedProgrammes.map((programme, index) => (
<Button
key={index}
variant="outline"
size="sm"
className="w-full justify-start h-auto p-2"
>
<Building2 className="h-4 w-4 mr-2" />
{programme.title}
</Button>
))}
</div>
</div>
) : (
<p className="text-sm text-muted-foreground">No linked programmes</p>
)}
</div>
<div>
<h4 className="font-medium mb-2">Course Metadata</h4>
<div className="space-y-2 text-sm">
<div className="flex justify-between">
<span className="text-muted-foreground">Total Duration:</span>
<span>{mockCourse.duration}</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">Modules:</span>
<span>{mockCourse.modules.length}</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">Total Lessons:</span>
<span>{mockCourse.modules.reduce((acc, module) => acc + module.lessons.length, 0)}</span>
</div>
</div>
</div>
</CardContent>
</Card>
</div>
</TabsContent>
{/* Syllabus Tab */}
<TabsContent value="syllabus" className="space-y-6 mt-6">
<div className="space-y-4">
<div className="flex items-center justify-between">
<div>
<h3 className="text-lg font-semibold">Course Syllabus</h3>
<p className="text-muted-foreground">Read-only view of the course structure and lessons</p>
</div>
<Badge variant="outline">Read Only</Badge>
</div>
<Accordion type="single" collapsible className="space-y-2">
{mockCourse.modules.map((module, index) => (
<AccordionItem key={index} value={`module-${index}`} className="border rounded-lg px-4">
<AccordionTrigger className="hover:no-underline">
<div className="flex items-center gap-3">
<Badge variant="outline">Module {index + 1}</Badge>
<span className="font-medium">{module.title}</span>
<span className="text-muted-foreground">({module.lessons.length} lessons)</span>
</div>
</AccordionTrigger>
<AccordionContent className="space-y-3 pb-4">
{module.lessons.map((lesson, lessonIndex) => (
<div key={lessonIndex} className="flex items-center justify-between p-3 bg-muted/30 rounded-lg">
<div className="flex items-center gap-3">
{getTypeIcon(lesson.type)}
<div>
<p className="font-medium">{lesson.title}</p>
<div className="flex items-center gap-4 text-sm text-muted-foreground">
<span className="flex items-center gap-1">
<Clock className="h-3 w-3" />
{lesson.eta}
</span>
{lesson.dueDate && (
<span className="flex items-center gap-1">
<AlertCircle className="h-3 w-3" />
Due: {lesson.dueDate}
</span>
)}
<Badge variant="outline" className="text-xs capitalize">
{lesson.type}
</Badge>
</div>
</div>
</div>
<Button variant="outline" size="sm">
<Eye className="h-4 w-4 mr-2" />
Open
</Button>
</div>
))}
</AccordionContent>
</AccordionItem>
))}
</Accordion>
</div>
</TabsContent>
{/* Learners Tab */}
<TabsContent value="learners" className="space-y-6 mt-6">
<div className="flex flex-col sm:flex-row gap-4 items-start sm:items-center justify-between">
<div>
<h3 className="text-lg font-semibold">Learner Progress</h3>
<p className="text-muted-foreground">Track individual progress through the course</p>
</div>
<div className="flex items-center gap-2">
<Button
variant="outline"
onClick={() => handleExport('excel')}
disabled={exporting}
className="min-tap-44"
>
{exporting ? (
<RefreshCw className="h-4 w-4 mr-2 animate-spin" />
) : (
<Download className="h-4 w-4 mr-2" />
)}
Export Data
</Button>
</div>
</div>
{/* Filters */}
<div className="flex flex-wrap gap-4 items-center">
<div className="relative flex-1 min-w-[200px] max-w-md">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
placeholder="Search learners..."
className="pl-10"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
</div>
<Select value={statusFilter} onValueChange={setStatusFilter}>
<SelectTrigger className="w-[150px]">
<SelectValue placeholder="Status" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Status</SelectItem>
<SelectItem value="Not Started">Not Started</SelectItem>
<SelectItem value="In-Progress">In Progress</SelectItem>
<SelectItem value="Completed">Completed</SelectItem>
</SelectContent>
</Select>
</div>
{/* Learners Table */}
<div className="rounded-md border">
<Table>
<TableHeader className="sticky-header">
<TableRow>
<TableHead className="w-[200px]">Learner</TableHead>
<TableHead className="w-[100px]">Progress</TableHead>
<TableHead className="w-[200px]">Current Lesson</TableHead>
<TableHead className="w-[120px]">Last Activity</TableHead>
<TableHead className="w-[100px]">Performance</TableHead>
<TableHead className="w-[80px]">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filteredLearners.map((learner) => (
<TableRow key={learner.id} className="min-h-[48px]">
<TableCell>
<div>
<p className="font-medium">{learner.name}</p>
<p className="text-sm text-muted-foreground">{learner.email}</p>
</div>
</TableCell>
<TableCell>
<div className="space-y-1">
<Progress value={learner.progressPct} className="w-16" />
<span className="text-sm">{learner.progressPct}%</span>
</div>
</TableCell>
<TableCell>
<div className="flex items-center gap-2">
{getTypeIcon('video')}
<div>
<p className="font-medium text-sm">{learner.currentLesson.title}</p>
{getStatusBadge(learner.currentLesson.status)}
</div>
</div>
</TableCell>
<TableCell className="text-sm text-muted-foreground">
{learner.lastActivity}
</TableCell>
<TableCell>
<div className="text-sm">
{learner.avgScore && (
<div>
<span className="font-medium">{learner.avgScore}%</span>
<p className="text-xs text-muted-foreground">
{learner.attempts} attempt{learner.attempts !== 1 ? 's' : ''}
</p>
</div>
)}
</div>
</TableCell>
<TableCell>
<div className="flex gap-1">
<Button
variant="ghost"
size="sm"
onClick={() => handleViewLearner(learner)}
className="min-tap-44"
aria-label={`View details for ${learner.name}`}
>
<Eye className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="sm"
className="min-tap-44"
aria-label={`Send reminder to ${learner.name}`}
>
<Mail className="h-4 w-4" />
</Button>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
</TabsContent>
{/* Reports Tab */}
<TabsContent value="reports" className="space-y-6 mt-6">
<div className="text-center py-12 text-muted-foreground">
<BarChart3 className="h-12 w-12 mx-auto mb-4" />
<h3 className="text-lg font-semibold mb-2">Course Reports</h3>
<p>Detailed analytics and reporting for this course would be displayed here</p>
</div>
</TabsContent>
{/* Activity Tab */}
<TabsContent value="activity" className="space-y-6 mt-6">
<div className="text-center py-12 text-muted-foreground">
<Activity className="h-12 w-12 mx-auto mb-4" />
<h3 className="text-lg font-semibold mb-2">Activity Log</h3>
<p>Course activity audit trail would be displayed here</p>
</div>
</TabsContent>
</Tabs>
</CardContent>
</Card>
{/* Learner Details Drawer */}
<Sheet open={showLearnerDrawer} onOpenChange={setShowLearnerDrawer}>
<SheetContent className="w-[480px] sm:w-[540px]">
<SheetHeader>
<SheetTitle>{selectedLearner?.name}</SheetTitle>
<SheetDescription>Course progress and performance details</SheetDescription>
</SheetHeader>
{selectedLearner && (
<div className="space-y-6 mt-6">
<div>
<h4 className="font-medium mb-2">Course Progress</h4>
<div className="space-y-2">
<div className="flex justify-between items-center">
<span className="text-muted-foreground">Overall Progress:</span>
<span className="font-medium">{selectedLearner.progressPct}%</span>
</div>
<Progress value={selectedLearner.progressPct} className="w-full" />
</div>
</div>
<div>
<h4 className="font-medium mb-2">Lesson Checklist</h4>
<div className="space-y-3">
{mockCourse.modules.flatMap(module =>
module.lessons.map((lesson, index) => (
<div key={index} className="flex items-center gap-3 p-2 rounded-lg bg-muted/30">
<div className={`w-4 h-4 rounded-full border-2 ${
lesson.status === 'Completed' ? 'bg-status-success border-status-success' :
lesson.status === 'In Progress' ? 'border-brand-primary bg-brand-primary' :
'border-muted'
}`}>
{lesson.status === 'Completed' && <CheckCircle className="w-4 h-4 text-white" />}
</div>
<div className="flex-1">
<div className="flex items-center gap-2">
{getTypeIcon(lesson.type)}
<span className={`text-sm ${
lesson.id === selectedLearner.currentLesson.id ? 'font-medium' : 'text-muted-foreground'
}`}>
{lesson.title}
</span>
</div>
<p className="text-xs text-muted-foreground">{lesson.eta}</p>
</div>
</div>
))
)}
</div>
</div>
{selectedLearner.avgScore && (
<div>
<h4 className="font-medium mb-2">Performance Summary</h4>
<div className="space-y-2 text-sm">
<div className="flex justify-between">
<span className="text-muted-foreground">Average Score:</span>
<span className="font-medium">{selectedLearner.avgScore}%</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">Quiz Attempts:</span>
<span>{selectedLearner.attempts}</span>
</div>
</div>
</div>
)}
<div className="flex gap-2">
<Button variant="outline" className="flex-1">
<Eye className="h-4 w-4 mr-2" />
View Detailed Report
</Button>
<Button variant="outline" className="flex-1">
<Mail className="h-4 w-4 mr-2" />
Send Reminder
</Button>
</div>
</div>
)}
</SheetContent>
</Sheet>
</div>
);
};

View File

@@ -0,0 +1,226 @@
import React, { useState } from 'react';
import { Button } from './ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from './ui/card';
import { Badge } from './ui/badge';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './ui/select';
import {
MessageSquare,
Eye,
Check,
Filter,
Users,
Clock
} from 'lucide-react';
interface ForumThread {
threadId: string;
title: string;
programmeId: string;
programmeName: string;
lastReplyAt: Date;
lastAuthor: string;
unread: boolean;
href: string;
replyCount?: number;
participantCount?: number;
}
interface DiscussionForumFeedProps {
forumFeed?: ForumThread[];
onOpenThread?: (threadId: string) => void;
onMarkAsRead?: (threadId: string) => void;
}
const mockForumFeed: ForumThread[] = [
{
threadId: 'thread-001',
title: 'Leadership in Remote Teams - Best Practices Discussion',
programmeId: 'prog-001',
programmeName: 'Leadership Development',
lastReplyAt: new Date('2024-12-27T15:30:00'),
lastAuthor: 'Sarah Chen',
unread: true,
href: '/discussions/leadership-remote-teams',
replyCount: 12,
participantCount: 8
},
{
threadId: 'thread-002',
title: 'JavaScript ES6 Features - Questions and Examples',
programmeId: 'prog-002',
programmeName: 'Technical Skills Bootcamp',
lastReplyAt: new Date('2024-12-27T14:15:00'),
lastAuthor: 'David Kim',
unread: true,
href: '/discussions/javascript-es6',
replyCount: 18,
participantCount: 15
},
{
threadId: 'thread-003',
title: 'Effective Presentation Techniques - Share Your Tips',
programmeId: 'prog-003',
programmeName: 'Communication Excellence',
lastReplyAt: new Date('2024-12-27T11:45:00'),
lastAuthor: 'Emma Thompson',
unread: false,
href: '/discussions/presentation-techniques',
replyCount: 7,
participantCount: 6
},
{
threadId: 'thread-004',
title: 'Agile vs Waterfall - When to Use Each Methodology',
programmeId: 'prog-004',
programmeName: 'Project Management Certification',
lastReplyAt: new Date('2024-12-27T09:20:00'),
lastAuthor: 'Michael Rodriguez',
unread: false,
href: '/discussions/agile-vs-waterfall',
replyCount: 25,
participantCount: 19
},
{
threadId: 'thread-005',
title: 'Building Trust in Virtual Teams',
programmeId: 'prog-001',
programmeName: 'Leadership Development',
lastReplyAt: new Date('2024-12-26T16:30:00'),
lastAuthor: 'Lisa Wang',
unread: true,
href: '/discussions/trust-virtual-teams',
replyCount: 9,
participantCount: 7
},
{
threadId: 'thread-006',
title: 'Code Review Best Practices - Peer Learning',
programmeId: 'prog-002',
programmeName: 'Technical Skills Bootcamp',
lastReplyAt: new Date('2024-12-26T14:10:00'),
lastAuthor: 'James Wilson',
unread: false,
href: '/discussions/code-review-practices',
replyCount: 14,
participantCount: 11
}
];
const programmeColors = {
'prog-001': '#04045B',
'prog-002': '#F8C301',
'prog-003': '#21a36a',
'prog-004': '#89002D'
};
export const DiscussionForumFeed: React.FC<DiscussionForumFeedProps> = ({
forumFeed = mockForumFeed,
onOpenThread,
onMarkAsRead
}) => {
// Only show unread threads by default to make it concise
const unreadThreads = forumFeed.filter(thread => thread.unread).slice(0, 5);
const formatTimeAgo = (date: Date) => {
const now = new Date();
const diffHours = Math.floor((now.getTime() - date.getTime()) / (1000 * 60 * 60));
if (diffHours < 1) {
return 'Just now';
} else if (diffHours < 24) {
return `${diffHours}h ago`;
} else {
const diffDays = Math.floor(diffHours / 24);
return `${diffDays}d ago`;
}
};
const handleOpenThread = (thread: ForumThread) => {
onOpenThread?.(thread.threadId);
console.log(`Opening thread: ${thread.title}`);
};
const handleMarkAsRead = (threadId: string, event: React.MouseEvent) => {
event.stopPropagation();
onMarkAsRead?.(threadId);
console.log(`Marked thread as read: ${threadId}`);
};
return (
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<div>
<CardTitle className="flex items-center gap-2">
<MessageSquare className="h-5 w-5" />
Discussion Forums
{unreadThreads.length > 0 && (
<Badge variant="destructive" className="text-xs">
{unreadThreads.length} new
</Badge>
)}
</CardTitle>
<CardDescription>Recent activity from programme discussions</CardDescription>
</div>
</div>
</CardHeader>
<CardContent>
<div className="space-y-2">
{unreadThreads.length === 0 ? (
<div className="text-center py-6 text-muted-foreground">
<MessageSquare className="h-8 w-8 mx-auto mb-2 text-muted-foreground" />
<p className="text-sm">No new discussion activity</p>
</div>
) : (
unreadThreads.map((thread) => (
<div
key={thread.threadId}
className="flex items-center justify-between p-3 border rounded-lg cursor-pointer transition-colors min-tap-44 bg-blue-50 border-blue-200 hover:bg-blue-100"
onClick={() => handleOpenThread(thread)}
role="button"
tabIndex={0}
aria-label={`Open discussion: ${thread.title}`}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
handleOpenThread(thread);
}
}}
>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1">
<h4 className="font-medium text-sm text-blue-900 truncate">
{thread.title}
</h4>
<Badge variant="secondary" className="text-xs bg-blue-100 text-blue-800 flex-shrink-0">
New
</Badge>
</div>
<div className="flex items-center gap-3 text-xs text-muted-foreground">
<span className="truncate">{thread.programmeName}</span>
<span></span>
<span>{formatTimeAgo(thread.lastReplyAt)}</span>
<span></span>
<span>by {thread.lastAuthor}</span>
</div>
</div>
<Button
variant="ghost"
size="sm"
onClick={(e) => handleMarkAsRead(thread.threadId, e)}
className="min-tap-44 ml-2"
aria-label={`Mark "${thread.title}" as read`}
>
<Check className="h-4 w-4" />
</Button>
</div>
))
)}
</div>
</CardContent>
</Card>
);
};

View File

@@ -0,0 +1,502 @@
import React, { useState } from 'react';
import { Button } from './ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from './ui/card';
import { Badge } from './ui/badge';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './ui/select';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from './ui/table';
import { Progress } from './ui/progress';
import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle, SheetTrigger } from './ui/sheet';
import {
Eye,
Send,
BarChart3,
User,
BookOpen,
Clock,
TrendingUp
} from 'lucide-react';
interface CurrentItem {
type: 'course' | 'content';
id: string;
title: string;
status: 'Not Started' | 'In-Progress' | 'Completed';
}
interface NextItem {
type: 'course' | 'content';
id: string;
title: string;
}
interface LearnerAnalytics {
learnerId: string;
learnerName: string;
learnerEmail: string;
currentItem?: CurrentItem;
progressPct: number;
nextItem?: NextItem;
lastActivity: Date;
}
interface LearningAnalyticsData {
programmeId: string;
rows: LearnerAnalytics[];
}
interface LearningAnalyticsTableProps {
analyticsData?: LearningAnalyticsData[];
onViewLearner?: (learnerId: string) => void;
onNudgeLearner?: (learnerId: string) => void;
onViewAllAnalytics?: (programmeId: string) => void;
}
const mockAnalyticsData: LearningAnalyticsData[] = [
{
programmeId: 'prog-001',
rows: [
{
learnerId: 'learner-001',
learnerName: 'Sarah Chen',
learnerEmail: 'sarah.chen@company.com',
currentItem: {
type: 'course',
id: 'course-001',
title: 'Strategic Thinking',
status: 'In-Progress'
},
progressPct: 85,
nextItem: {
type: 'course',
id: 'course-002',
title: 'Decision Making'
},
lastActivity: new Date('2024-12-27T14:30:00')
},
{
learnerId: 'learner-002',
learnerName: 'Michael Rodriguez',
learnerEmail: 'michael.r@company.com',
currentItem: {
type: 'content',
id: 'content-003',
title: 'Leadership Styles Assessment',
status: 'Not Started'
},
progressPct: 62,
nextItem: {
type: 'course',
id: 'course-003',
title: 'Team Management'
},
lastActivity: new Date('2024-12-26T09:15:00')
},
{
learnerId: 'learner-003',
learnerName: 'Emma Thompson',
learnerEmail: 'emma.thompson@company.com',
currentItem: {
type: 'course',
id: 'course-002',
title: 'Decision Making',
status: 'Completed'
},
progressPct: 94,
nextItem: {
type: 'content',
id: 'content-005',
title: 'Leadership Reflection Journal'
},
lastActivity: new Date('2024-12-27T16:45:00')
},
{
learnerId: 'learner-004',
learnerName: 'David Kim',
learnerEmail: 'david.kim@company.com',
currentItem: {
type: 'course',
id: 'course-001',
title: 'Strategic Thinking',
status: 'In-Progress'
},
progressPct: 78,
nextItem: {
type: 'course',
id: 'course-002',
title: 'Decision Making'
},
lastActivity: new Date('2024-12-27T11:20:00')
}
]
},
{
programmeId: 'prog-002',
rows: [
{
learnerId: 'learner-005',
learnerName: 'Lisa Wang',
learnerEmail: 'lisa.wang@company.com',
currentItem: {
type: 'course',
id: 'course-101',
title: 'JavaScript Fundamentals',
status: 'In-Progress'
},
progressPct: 56,
nextItem: {
type: 'course',
id: 'course-102',
title: 'React Basics'
},
lastActivity: new Date('2024-12-27T13:10:00')
},
{
learnerId: 'learner-006',
learnerName: 'James Wilson',
learnerEmail: 'james.wilson@company.com',
currentItem: {
type: 'content',
id: 'content-201',
title: 'API Design Best Practices',
status: 'Not Started'
},
progressPct: 34,
nextItem: {
type: 'course',
id: 'course-103',
title: 'Database Design'
},
lastActivity: new Date('2024-12-25T15:30:00')
}
]
}
];
const programmeNames = {
'prog-001': 'Leadership Development',
'prog-002': 'Technical Skills Bootcamp',
'prog-003': 'Communication Excellence',
'prog-004': 'Project Management Certification'
};
export const LearningAnalyticsTable: React.FC<LearningAnalyticsTableProps> = ({
analyticsData = mockAnalyticsData,
onViewLearner,
onNudgeLearner,
onViewAllAnalytics
}) => {
const [selectedProgramme, setSelectedProgramme] = useState(analyticsData[0]?.programmeId || '');
const [selectedLearner, setSelectedLearner] = useState<LearnerAnalytics | null>(null);
const [isLearnerDrawerOpen, setIsLearnerDrawerOpen] = useState(false);
const currentProgrammeData = analyticsData.find(data => data.programmeId === selectedProgramme);
const currentProgrammeName = programmeNames[selectedProgramme as keyof typeof programmeNames] || 'Unknown Programme';
const formatLastActivity = (date: Date) => {
const now = new Date();
const diffHours = Math.floor((now.getTime() - date.getTime()) / (1000 * 60 * 60));
if (diffHours < 1) {
return 'Just now';
} else if (diffHours < 24) {
return `${diffHours} hours ago`;
} else {
const diffDays = Math.floor(diffHours / 24);
return `${diffDays} day${diffDays !== 1 ? 's' : ''} ago`;
}
};
const getStatusBadgeProps = (status: CurrentItem['status']) => {
switch (status) {
case 'Completed':
return { variant: 'default' as const, className: 'bg-status-success text-status-success-foreground' };
case 'In-Progress':
return { variant: 'secondary' as const, className: 'bg-status-warn text-status-warn-foreground' };
case 'Not Started':
return { variant: 'outline' as const, className: 'border-status-error text-status-error' };
default:
return { variant: 'secondary' as const };
}
};
const handleViewLearner = (learner: LearnerAnalytics) => {
setSelectedLearner(learner);
setIsLearnerDrawerOpen(true);
onViewLearner?.(learner.learnerId);
};
const handleNudgeLearner = (learnerId: string) => {
onNudgeLearner?.(learnerId);
console.log(`Sent nudge to learner: ${learnerId}`);
};
const handleViewAllAnalytics = () => {
onViewAllAnalytics?.(selectedProgramme);
console.log(`Viewing all analytics for programme: ${selectedProgramme}`);
};
return (
<Card>
<CardHeader>
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div>
<CardTitle>Learning Analytics</CardTitle>
<CardDescription>Per-programme learner progress and current activities</CardDescription>
</div>
<div className="flex items-center gap-2">
<Select value={selectedProgramme} onValueChange={setSelectedProgramme}>
<SelectTrigger className="w-[250px]">
<SelectValue placeholder="Select programme" />
</SelectTrigger>
<SelectContent>
{analyticsData.map((data) => (
<SelectItem key={data.programmeId} value={data.programmeId}>
{programmeNames[data.programmeId as keyof typeof programmeNames] || data.programmeId}
</SelectItem>
))}
</SelectContent>
</Select>
<Button
variant="outline"
onClick={handleViewAllAnalytics}
className="min-tap-44"
>
<BarChart3 className="h-4 w-4 mr-2" />
View All in Analytics
</Button>
</div>
</div>
</CardHeader>
<CardContent>
<div className="space-y-4">
{/* Programme Summary */}
<div className="flex items-center justify-between p-4 bg-muted/30 rounded-lg">
<div>
<h4 className="font-medium">{currentProgrammeName}</h4>
<p className="text-sm text-muted-foreground">
{currentProgrammeData?.rows.length || 0} learners enrolled
</p>
</div>
<div className="text-right">
<p className="text-2xl font-bold">
{currentProgrammeData ? Math.round(
currentProgrammeData.rows.reduce((sum, learner) => sum + learner.progressPct, 0) /
currentProgrammeData.rows.length
) : 0}%
</p>
<p className="text-sm text-muted-foreground">Average Progress</p>
</div>
</div>
{/* Analytics Table */}
<div className="rounded-md border">
<Table>
<TableHeader className="sticky-header">
<TableRow>
<TableHead className="w-[200px]">Learner</TableHead>
<TableHead className="w-[250px]">Current Item</TableHead>
<TableHead className="w-[120px]">Progress</TableHead>
<TableHead className="w-[200px]">Next Item</TableHead>
<TableHead className="w-[120px]">Last Activity</TableHead>
<TableHead className="w-[120px]">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{currentProgrammeData?.rows.map((learner) => (
<TableRow key={learner.learnerId} className="min-h-[44px]">
<TableCell>
<div>
<p className="font-medium">{learner.learnerName}</p>
<p className="text-sm text-muted-foreground">{learner.learnerEmail}</p>
</div>
</TableCell>
<TableCell>
{learner.currentItem ? (
<div className="space-y-1">
<div className="flex items-center gap-2">
{learner.currentItem.type === 'course' ? (
<BookOpen className="h-4 w-4 text-muted-foreground" />
) : (
<div className="h-4 w-4 bg-muted-foreground rounded-sm" />
)}
<span className="text-sm font-medium">{learner.currentItem.title}</span>
</div>
<Badge
{...getStatusBadgeProps(learner.currentItem.status)}
className="text-xs"
>
{learner.currentItem.status}
</Badge>
</div>
) : (
<span className="text-muted-foreground text-sm">No current item</span>
)}
</TableCell>
<TableCell>
<div className="space-y-1">
<div className="flex items-center gap-2">
<Progress
value={learner.progressPct}
className="w-16"
aria-describedby={`progress-${learner.learnerId}`}
/>
<span className="text-sm font-medium">{learner.progressPct}%</span>
</div>
<span id={`progress-${learner.learnerId}`} className="sr-only">
Progress: {learner.progressPct} percent complete
</span>
</div>
</TableCell>
<TableCell>
{learner.nextItem ? (
<div className="flex items-center gap-2">
{learner.nextItem.type === 'course' ? (
<BookOpen className="h-4 w-4 text-muted-foreground" />
) : (
<div className="h-4 w-4 bg-muted-foreground rounded-sm" />
)}
<span className="text-sm">{learner.nextItem.title}</span>
</div>
) : (
<span className="text-muted-foreground text-sm">Programme complete</span>
)}
</TableCell>
<TableCell>
<div className="flex items-center gap-1 text-sm text-muted-foreground">
<Clock className="h-4 w-4" />
<span>{formatLastActivity(learner.lastActivity)}</span>
</div>
</TableCell>
<TableCell>
<div className="flex items-center gap-1">
<Button
variant="ghost"
size="sm"
onClick={() => handleViewLearner(learner)}
className="min-tap-44"
aria-label={`View details for ${learner.learnerName}`}
>
<Eye className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => handleNudgeLearner(learner.learnerId)}
className="min-tap-44"
aria-label={`Send reminder to ${learner.learnerName}`}
>
<Send className="h-4 w-4" />
</Button>
</div>
</TableCell>
</TableRow>
)) || (
<TableRow>
<TableCell colSpan={6} className="text-center py-8 text-muted-foreground">
No learner data available for this programme.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
</div>
{/* Learner Detail Drawer */}
<Sheet open={isLearnerDrawerOpen} onOpenChange={setIsLearnerDrawerOpen}>
<SheetContent
className="w-[500px] sm:w-[600px]"
role="dialog"
aria-modal="true"
aria-labelledby="learner-detail-title"
>
<SheetHeader>
<SheetTitle id="learner-detail-title">
{selectedLearner?.learnerName}
</SheetTitle>
<SheetDescription>
Detailed progress and activity in {currentProgrammeName}
</SheetDescription>
</SheetHeader>
{selectedLearner && (
<div className="mt-6 space-y-6">
{/* Contact Info */}
<div className="space-y-2">
<h4 className="font-medium">Contact Information</h4>
<div className="space-y-1">
<p className="text-sm text-muted-foreground">{selectedLearner.learnerEmail}</p>
<p className="text-sm text-muted-foreground">
Last active: {formatLastActivity(selectedLearner.lastActivity)}
</p>
</div>
</div>
{/* Progress Summary */}
<div className="space-y-2">
<h4 className="font-medium">Progress Summary</h4>
<div className="p-4 bg-muted/30 rounded-lg">
<div className="flex items-center justify-between mb-2">
<span className="text-sm">Overall Progress</span>
<span className="font-medium">{selectedLearner.progressPct}%</span>
</div>
<Progress value={selectedLearner.progressPct} className="h-2" />
</div>
</div>
{/* Current Activity */}
{selectedLearner.currentItem && (
<div className="space-y-2">
<h4 className="font-medium">Current Activity</h4>
<div className="p-4 border rounded-lg">
<div className="flex items-center gap-2 mb-2">
{selectedLearner.currentItem.type === 'course' ? (
<BookOpen className="h-4 w-4 text-muted-foreground" />
) : (
<div className="h-4 w-4 bg-muted-foreground rounded-sm" />
)}
<span className="font-medium">{selectedLearner.currentItem.title}</span>
</div>
<Badge {...getStatusBadgeProps(selectedLearner.currentItem.status)}>
{selectedLearner.currentItem.status}
</Badge>
</div>
</div>
)}
{/* Next Up */}
{selectedLearner.nextItem && (
<div className="space-y-2">
<h4 className="font-medium">Next Up</h4>
<div className="p-4 border border-dashed rounded-lg">
<div className="flex items-center gap-2">
{selectedLearner.nextItem.type === 'course' ? (
<BookOpen className="h-4 w-4 text-muted-foreground" />
) : (
<div className="h-4 w-4 bg-muted-foreground rounded-sm" />
)}
<span>{selectedLearner.nextItem.title}</span>
</div>
</div>
</div>
)}
{/* Actions */}
<div className="flex gap-2 pt-4">
<Button className="flex-1">
<Send className="h-4 w-4 mr-2" />
Send Reminder
</Button>
<Button variant="outline" className="flex-1">
<TrendingUp className="h-4 w-4 mr-2" />
View Full Analytics
</Button>
</div>
</div>
)}
</SheetContent>
</Sheet>
</CardContent>
</Card>
);
};

View File

@@ -0,0 +1,445 @@
import React, { useState } from 'react';
import { Button } from './ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from './ui/card';
import { Badge } from './ui/badge';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './ui/select';
import { Checkbox } from './ui/checkbox';
import {
Calendar as CalendarIcon,
ChevronLeft,
ChevronRight,
Video,
School,
Clock,
Filter
} from 'lucide-react';
interface CalendarEvent {
id: string;
programmeId: string;
type: 'webinar' | 'class' | 'course_end' | 'content_end' | 'programme_end';
title: string;
start: Date;
end?: Date;
href: string;
}
interface ProgrammeLegend {
programmeId: string;
programmeName: string;
color: string;
}
interface CalendarData {
legend: ProgrammeLegend[];
events: CalendarEvent[];
}
interface ProgrammeCalendarProps {
calendarData?: CalendarData;
onEventClick?: (event: CalendarEvent) => void;
}
const mockCalendarData: CalendarData = {
legend: [
{ programmeId: 'prog-001', programmeName: 'Leadership Development', color: '#04045B' },
{ programmeId: 'prog-002', programmeName: 'Technical Skills', color: '#F8C301' },
{ programmeId: 'prog-003', programmeName: 'Communication', color: '#21a36a' },
{ programmeId: 'prog-004', programmeName: 'Project Management', color: '#89002D' },
{ programmeId: 'prog-005', programmeName: 'Sales Training', color: '#C0C0C0' },
{ programmeId: 'prog-006', programmeName: 'Analytics Program', color: '#6366f1' }
],
events: [
{
id: 'event-001',
programmeId: 'prog-001',
type: 'webinar',
title: 'Leadership Fundamentals Webinar',
start: new Date('2024-12-28T14:00:00'),
end: new Date('2024-12-28T15:30:00'),
href: '/webinars/leadership-fundamentals'
},
{
id: 'event-002',
programmeId: 'prog-002',
type: 'class',
title: 'Hands-on Coding Workshop',
start: new Date('2024-12-30T09:00:00'),
end: new Date('2024-12-30T17:00:00'),
href: '/classes/coding-workshop'
},
{
id: 'event-003',
programmeId: 'prog-001',
type: 'course_end',
title: 'Strategic Thinking Course Due',
start: new Date('2025-01-02T23:59:00'),
href: '/courses/strategic-thinking'
},
{
id: 'event-004',
programmeId: 'prog-003',
type: 'webinar',
title: 'Public Speaking Masterclass',
start: new Date('2025-01-05T11:00:00'),
end: new Date('2025-01-05T12:30:00'),
href: '/webinars/public-speaking'
},
{
id: 'event-005',
programmeId: 'prog-004',
type: 'programme_end',
title: 'Project Management Certification Due',
start: new Date('2025-01-10T23:59:00'),
href: '/programmes/project-management'
}
]
};
export const ProgrammeCalendar: React.FC<ProgrammeCalendarProps> = ({
calendarData = mockCalendarData,
onEventClick
}) => {
const [currentDate, setCurrentDate] = useState(new Date());
const [viewMode, setViewMode] = useState<'month' | 'week'>('month');
const [selectedProgrammes, setSelectedProgrammes] = useState<string[]>(
calendarData.legend.slice(0, 6).map(p => p.programmeId)
);
const [selectedEventTypes, setSelectedEventTypes] = useState<string[]>([
'webinar', 'class', 'course_end', 'content_end', 'programme_end'
]);
const [showFilters, setShowFilters] = useState(false);
const visibleLegend = calendarData.legend.slice(0, 6);
const additionalProgrammes = calendarData.legend.length - 6;
const getEventIcon = (type: CalendarEvent['type']) => {
switch (type) {
case 'webinar':
return <Video className="h-3 w-3" />;
case 'class':
return <School className="h-3 w-3" />;
default:
return <Clock className="h-3 w-3" />;
}
};
const getEventTypeLabel = (type: CalendarEvent['type']) => {
switch (type) {
case 'webinar':
return 'Webinar';
case 'class':
return 'Offline Class';
case 'course_end':
return 'Course End';
case 'content_end':
return 'Content End';
case 'programme_end':
return 'Programme End';
default:
return 'Event';
}
};
const filteredEvents = calendarData.events.filter(event =>
selectedProgrammes.includes(event.programmeId) &&
selectedEventTypes.includes(event.type)
);
const formatEventTime = (start: Date, end?: Date) => {
const startTime = start.toLocaleTimeString('en-AU', {
hour: '2-digit',
minute: '2-digit',
hour12: true
});
if (end) {
const endTime = end.toLocaleTimeString('en-AU', {
hour: '2-digit',
minute: '2-digit',
hour12: true
});
return `${startTime} - ${endTime}`;
}
return `Due ${startTime}`;
};
const getProgrammeColor = (programmeId: string) => {
const programme = calendarData.legend.find(p => p.programmeId === programmeId);
return programme?.color || '#6b7280';
};
const navigateMonth = (direction: 'prev' | 'next') => {
setCurrentDate(prev => {
const newDate = new Date(prev);
if (direction === 'prev') {
newDate.setMonth(prev.getMonth() - 1);
} else {
newDate.setMonth(prev.getMonth() + 1);
}
return newDate;
});
};
const toggleProgrammeFilter = (programmeId: string) => {
setSelectedProgrammes(prev =>
prev.includes(programmeId)
? prev.filter(id => id !== programmeId)
: [...prev, programmeId]
);
};
const toggleEventTypeFilter = (eventType: string) => {
setSelectedEventTypes(prev =>
prev.includes(eventType)
? prev.filter(type => type !== eventType)
: [...prev, eventType]
);
};
const handleEventClick = (event: CalendarEvent) => {
onEventClick?.(event);
console.log(`Clicked event: ${event.title}`);
};
return (
<Card>
<CardHeader>
<div className="flex flex-col gap-4">
<div className="flex items-center justify-between">
<div>
<CardTitle>Programme Schedule</CardTitle>
<CardDescription>Webinars, classes, and important deadlines</CardDescription>
</div>
<div className="flex items-center gap-2">
<Button
variant="outline"
size="sm"
onClick={() => setShowFilters(!showFilters)}
className="min-tap-44"
aria-label="Toggle filters"
>
<Filter className="h-4 w-4 mr-2" />
Filters
</Button>
<div className="flex border rounded-md">
<Button
variant={viewMode === 'month' ? 'default' : 'ghost'}
size="sm"
onClick={() => setViewMode('month')}
className="rounded-r-none"
>
Month
</Button>
<Button
variant={viewMode === 'week' ? 'default' : 'ghost'}
size="sm"
onClick={() => setViewMode('week')}
className="rounded-l-none"
>
Week
</Button>
</div>
</div>
</div>
{/* Calendar Navigation */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Button
variant="outline"
size="sm"
onClick={() => navigateMonth('prev')}
className="min-tap-44"
aria-label="Previous month"
>
<ChevronLeft className="h-4 w-4" />
</Button>
<h3 className="font-semibold text-lg">
{currentDate.toLocaleDateString('en-AU', {
month: 'long',
year: 'numeric'
})}
</h3>
<Button
variant="outline"
size="sm"
onClick={() => navigateMonth('next')}
className="min-tap-44"
aria-label="Next month"
>
<ChevronRight className="h-4 w-4" />
</Button>
</div>
<Button
variant="ghost"
size="sm"
onClick={() => setCurrentDate(new Date())}
className="min-tap-44"
>
Today
</Button>
</div>
{/* Programme Legend */}
<div className="space-y-2">
<h4 className="text-sm font-medium">Programmes</h4>
<div className="flex flex-wrap gap-2">
{visibleLegend.map((programme) => (
<div
key={programme.programmeId}
className="flex items-center gap-1"
>
<div
className="w-3 h-3 rounded-full"
style={{ backgroundColor: programme.color }}
aria-hidden="true"
/>
<span className="text-sm">{programme.programmeName}</span>
</div>
))}
{additionalProgrammes > 0 && (
<Badge variant="secondary" className="text-xs">
+{additionalProgrammes} more
</Badge>
)}
</div>
</div>
{/* Filters Panel */}
{showFilters && (
<div className="border rounded-lg p-4 space-y-4 bg-muted/30">
<div>
<h4 className="text-sm font-medium mb-2">Filter by Programme</h4>
<div className="space-y-2">
{calendarData.legend.map((programme) => (
<div key={programme.programmeId} className="flex items-center space-x-2">
<Checkbox
id={`prog-${programme.programmeId}`}
checked={selectedProgrammes.includes(programme.programmeId)}
onCheckedChange={() => toggleProgrammeFilter(programme.programmeId)}
className="min-tap-44"
/>
<label
htmlFor={`prog-${programme.programmeId}`}
className="text-sm font-medium cursor-pointer flex items-center gap-2"
>
<div
className="w-3 h-3 rounded-full"
style={{ backgroundColor: programme.color }}
aria-hidden="true"
/>
{programme.programmeName}
</label>
</div>
))}
</div>
</div>
<div>
<h4 className="text-sm font-medium mb-2">Filter by Type</h4>
<div className="space-y-2">
{[
{ value: 'webinar', label: 'Webinars' },
{ value: 'class', label: 'Offline Classes' },
{ value: 'course_end', label: 'Course End Dates' },
{ value: 'content_end', label: 'Content End Dates' },
{ value: 'programme_end', label: 'Programme End Dates' }
].map((eventType) => (
<div key={eventType.value} className="flex items-center space-x-2">
<Checkbox
id={`type-${eventType.value}`}
checked={selectedEventTypes.includes(eventType.value)}
onCheckedChange={() => toggleEventTypeFilter(eventType.value)}
className="min-tap-44"
/>
<label
htmlFor={`type-${eventType.value}`}
className="text-sm font-medium cursor-pointer"
>
{eventType.label}
</label>
</div>
))}
</div>
</div>
</div>
)}
</div>
</CardHeader>
<CardContent>
{/* Calendar Grid */}
<div
className="border rounded-lg p-4 min-h-[400px]"
role="grid"
aria-label={`Calendar for ${currentDate.toLocaleDateString('en-AU', { month: 'long', year: 'numeric' })}`}
>
{/* Events List View (simplified for demo) */}
<div className="space-y-3">
{filteredEvents.length === 0 ? (
<div className="text-center py-8 text-muted-foreground">
<CalendarIcon className="h-12 w-12 mx-auto mb-4 text-muted-foreground" />
<p>No events found for the selected filters.</p>
<p className="text-sm mt-1">Try adjusting your programme or event type filters.</p>
</div>
) : (
filteredEvents.map((event) => {
const programme = calendarData.legend.find(p => p.programmeId === event.programmeId);
return (
<div
key={event.id}
className="flex items-center justify-between p-3 border rounded-lg hover:bg-muted/50 cursor-pointer transition-colors min-tap-44"
onClick={() => handleEventClick(event)}
role="button"
tabIndex={0}
aria-label={`${event.title} on ${event.start.toLocaleDateString()}`}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
handleEventClick(event);
}
}}
>
<div className="flex items-center gap-3 flex-1">
<div
className="w-4 h-4 rounded-full flex items-center justify-center text-white"
style={{ backgroundColor: getProgrammeColor(event.programmeId) }}
aria-hidden="true"
>
{getEventIcon(event.type)}
</div>
<div className="flex-1">
<div className="flex items-center gap-2">
<span className="font-medium">{event.title}</span>
<Badge variant="outline" className="text-xs">
{getEventTypeLabel(event.type)}
</Badge>
</div>
<p className="text-sm text-muted-foreground">{programme?.programmeName}</p>
</div>
</div>
<div className="text-right">
<p className="text-sm font-medium">
{event.start.toLocaleDateString('en-AU', {
weekday: 'short',
month: 'short',
day: 'numeric'
})}
</p>
<p className="text-xs text-muted-foreground">
{formatEventTime(event.start, event.end)}
</p>
</div>
</div>
);
})
)}
</div>
</div>
</CardContent>
</Card>
);
};

View File

@@ -0,0 +1,902 @@
import React, { useState, useEffect } from 'react';
import { Button } from './ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from './ui/card';
import { Badge } from './ui/badge';
import { Input } from './ui/input';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './ui/select';
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from './ui/dialog';
import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle, SheetTrigger } from './ui/sheet';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from './ui/table';
import { Progress } from './ui/progress';
import { Tabs, TabsContent, TabsList, TabsTrigger } from './ui/tabs';
import { Alert, AlertDescription } from './ui/alert';
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from './ui/accordion';
import {
ArrowLeft,
Download,
BarChart3,
MoreHorizontal,
Users,
Calendar,
Clock,
BookOpen,
Award,
Bell,
Search,
Filter,
Eye,
Mail,
FileText,
Play,
MapPin,
Video,
FileQuestion,
Activity,
Building2,
RefreshCw,
ExternalLink,
Plus,
CheckCircle,
AlertCircle,
XCircle
} from 'lucide-react';
// Types
interface Programme {
id: string;
title: string;
status: 'Active' | 'Upcoming' | 'Completed';
code: string;
owner: string;
version: number;
description: string;
goals: string[];
tags: string[];
structure: {
preAssessment: ProgrammeItem[];
preLearning: ProgrammeItem[];
classroom: ProgrammeItem[];
postLearning: ProgrammeItem[];
};
}
interface ProgrammeItem {
id: string;
type: 'Profiler' | 'Course' | 'Content' | 'Webinar' | 'OfflineSession';
title: string;
duration?: string;
dueDate?: string;
venue?: string;
room?: string;
date?: string;
capacity?: number;
status?: 'Not Started' | 'In Progress' | 'Completed';
}
interface Assignment {
startDate: string;
endDate: string;
orgId: string;
orgName: string;
}
interface ProgrammeCounts {
learners: number;
completionPct: number;
courses: number;
content: number;
webinars: number;
classes: number;
}
interface ProgrammeLearner {
id: string;
name: string;
email: string;
currentItem: {
type: string;
id: string;
title: string;
status: 'Not Started' | 'In-Progress' | 'Completed';
};
progressPct: number;
nextItem?: {
type: string;
id: string;
title: string;
};
lastActivity: string;
stage: 'Pre-assessment' | 'Pre-learning' | 'Classroom' | 'Post-learning';
cohort?: string;
}
interface ProgrammeEvent {
id: string;
programmeId: string;
type: 'webinar' | 'class' | 'course_end' | 'content_end' | 'programme_end';
title: string;
start: string;
end: string;
venue?: string;
room?: string;
}
interface ProgrammeHRViewProps {
programmeId: string;
onBack: () => void;
onAssignLearners: (programmeId: string) => void;
onDownloadTracker: (programmeId: string) => void;
onOpenAnalytics: (programmeId: string) => void;
}
// Mock data
const mockProgramme: Programme = {
id: 'prg_123',
title: 'Executive Leadership Development Programme',
status: 'Active',
code: 'ELDP-2024',
owner: 'Dr. Sarah Johnson',
version: 2,
description: 'A comprehensive leadership development programme designed to build strategic thinking, emotional intelligence, and decision-making capabilities for senior executives.',
goals: [
'Develop strategic thinking and planning capabilities',
'Enhance emotional intelligence and self-awareness',
'Build effective communication and influence skills',
'Master change management and innovation leadership'
],
tags: ['Leadership', 'Executive', 'Strategic Thinking', 'Management'],
structure: {
preAssessment: [
{ id: 'pa1', type: 'Profiler', title: 'Leadership Style Assessment', status: 'Completed' },
{ id: 'pa2', type: 'Profiler', title: '360-Degree Feedback', status: 'In Progress' }
],
preLearning: [
{ id: 'pl1', type: 'Course', title: 'Strategic Thinking Fundamentals', duration: '4 hours', dueDate: '2024-01-15', status: 'Completed' },
{ id: 'pl2', type: 'Content', title: 'Leadership in Crisis Webcast', duration: '45 mins', status: 'In Progress' },
{ id: 'pl3', type: 'Webinar', title: 'Future of Leadership', date: '2024-01-20 10:00 AM AEDT', status: 'Not Started' }
],
classroom: [
{ id: 'c1', type: 'OfflineSession', title: 'Strategic Leadership Workshop', venue: 'Sydney Campus', room: 'Executive Suite A', date: '2024-02-05', capacity: 20 },
{ id: 'c2', type: 'OfflineSession', title: 'Case Study Analysis', venue: 'Sydney Campus', room: 'Conference Room B', date: '2024-02-06', capacity: 20 }
],
postLearning: [
{ id: 'po1', type: 'Course', title: 'Advanced Decision Making', duration: '6 hours', dueDate: '2024-02-20', status: 'Not Started' },
{ id: 'po2', type: 'Content', title: 'Leadership Reflection Journal', duration: '2 weeks', status: 'Not Started' }
]
}
};
const mockAssignment: Assignment = {
startDate: '2024-01-01',
endDate: '2024-03-31',
orgId: 'org_123',
orgName: 'Tech Solutions Pvt Ltd'
};
const mockCounts: ProgrammeCounts = {
learners: 28,
completionPct: 64,
courses: 2,
content: 2,
webinars: 1,
classes: 2
};
const mockLearners: ProgrammeLearner[] = [
{
id: 'l1',
name: 'Sarah Chen',
email: 'sarah.chen@company.com',
currentItem: { type: 'Course', id: 'pl1', title: 'Strategic Thinking Fundamentals', status: 'In-Progress' },
progressPct: 75,
nextItem: { type: 'Content', id: 'pl2', title: 'Leadership in Crisis Webcast' },
lastActivity: '2 hours ago',
stage: 'Pre-learning',
cohort: 'Cohort A'
},
{
id: 'l2',
name: 'Michael Rodriguez',
email: 'michael.r@company.com',
currentItem: { type: 'Profiler', id: 'pa2', title: '360-Degree Feedback', status: 'In-Progress' },
progressPct: 45,
nextItem: { type: 'Course', id: 'pl1', title: 'Strategic Thinking Fundamentals' },
lastActivity: '1 day ago',
stage: 'Pre-assessment',
cohort: 'Cohort A'
},
{
id: 'l3',
name: 'Emma Thompson',
email: 'emma.thompson@company.com',
currentItem: { type: 'OfflineSession', id: 'c1', title: 'Strategic Leadership Workshop', status: 'Completed' },
progressPct: 89,
nextItem: { type: 'Course', id: 'po1', title: 'Advanced Decision Making' },
lastActivity: '3 hours ago',
stage: 'Classroom',
cohort: 'Cohort B'
}
];
const mockEvents: ProgrammeEvent[] = [
{
id: 'e1',
programmeId: 'prg_123',
type: 'webinar',
title: 'Future of Leadership',
start: '2024-01-20T10:00:00',
end: '2024-01-20T11:30:00'
},
{
id: 'e2',
programmeId: 'prg_123',
type: 'class',
title: 'Strategic Leadership Workshop',
start: '2024-02-05T09:00:00',
end: '2024-02-05T17:00:00',
venue: 'Sydney Campus',
room: 'Executive Suite A'
}
];
export const ProgrammeHRView: React.FC<ProgrammeHRViewProps> = ({
programmeId,
onBack,
onAssignLearners,
onDownloadTracker,
onOpenAnalytics
}) => {
const [activeTab, setActiveTab] = useState('overview');
const [loading, setLoading] = useState(true);
const [searchTerm, setSearchTerm] = useState('');
const [statusFilter, setStatusFilter] = useState('all');
const [stageFilter, setStageFilter] = useState('all');
const [cohortFilter, setCohortFilter] = useState('all');
const [selectedLearner, setSelectedLearner] = useState<ProgrammeLearner | null>(null);
const [showLearnerDrawer, setShowLearnerDrawer] = useState(false);
const [exporting, setExporting] = useState(false);
// Simulate loading
useEffect(() => {
const timer = setTimeout(() => setLoading(false), 800);
return () => clearTimeout(timer);
}, []);
const getTypeIcon = (type: string) => {
switch (type) {
case 'Course': return <BookOpen className="h-4 w-4" />;
case 'Content': return <FileText className="h-4 w-4" />;
case 'Webinar': return <Video className="h-4 w-4" />;
case 'Profiler': return <FileQuestion className="h-4 w-4" />;
case 'OfflineSession': return <Building2 className="h-4 w-4" />;
default: return <BookOpen className="h-4 w-4" />;
}
};
const getStatusBadge = (status?: string) => {
switch (status) {
case 'Completed':
return <Badge variant="default" className="bg-status-success text-white"><CheckCircle className="h-3 w-3 mr-1" />Completed</Badge>;
case 'In Progress':
return <Badge variant="secondary" className="bg-status-warn text-black"><AlertCircle className="h-3 w-3 mr-1" />In Progress</Badge>;
case 'Not Started':
return <Badge variant="outline"><XCircle className="h-3 w-3 mr-1" />Not Started</Badge>;
default:
return null;
}
};
const filteredLearners = mockLearners.filter(learner => {
const matchesSearch = learner.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
learner.email.toLowerCase().includes(searchTerm.toLowerCase());
const matchesStatus = statusFilter === 'all' || learner.currentItem.status === statusFilter;
const matchesStage = stageFilter === 'all' || learner.stage === stageFilter;
const matchesCohort = cohortFilter === 'all' || learner.cohort === cohortFilter;
return matchesSearch && matchesStatus && matchesStage && matchesCohort;
});
const handleViewLearner = (learner: ProgrammeLearner) => {
setSelectedLearner(learner);
setShowLearnerDrawer(true);
};
const handleExport = async (format: 'excel' | 'csv' | 'pdf') => {
setExporting(true);
await new Promise(resolve => setTimeout(resolve, 2000));
setExporting(false);
console.log(`Exported programme tracker as ${format.toUpperCase()}`);
};
if (loading) {
return (
<div className="space-y-6 animate-pulse">
<div className="h-16 bg-muted rounded-lg"></div>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
{[...Array(4)].map((_, i) => (
<div key={i} className="h-24 bg-muted rounded-lg"></div>
))}
</div>
<div className="h-96 bg-muted rounded-lg"></div>
</div>
);
}
return (
<div className="space-y-6">
{/* Header */}
<Card className="sticky top-0 z-20 bg-background border-b shadow-sm">
<CardContent className="pt-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<Button
variant="ghost"
size="icon"
onClick={onBack}
className="min-tap-44"
aria-label="Go back to programmes list"
>
<ArrowLeft className="h-4 w-4" />
</Button>
<div>
<div className="flex items-center gap-3 mb-1">
<h1 className="text-2xl font-bold">{mockProgramme.title}</h1>
<Badge
variant={mockProgramme.status === 'Active' ? 'default' : 'secondary'}
className={mockProgramme.status === 'Active' ? 'bg-status-success' : ''}
>
{mockProgramme.status}
</Badge>
</div>
<p className="text-muted-foreground">
{mockProgramme.code} {mockProgramme.owner} Version {mockProgramme.version}
</p>
</div>
</div>
<div className="flex items-center gap-2">
<Button
onClick={() => onAssignLearners(programmeId)}
className="min-tap-44"
>
<Users className="h-4 w-4 mr-2" />
Assign Learners
</Button>
<Button
variant="outline"
onClick={() => onDownloadTracker(programmeId)}
disabled={exporting}
className="min-tap-44"
>
{exporting ? (
<RefreshCw className="h-4 w-4 mr-2 animate-spin" />
) : (
<Download className="h-4 w-4 mr-2" />
)}
Download Tracker
</Button>
<Button
variant="outline"
onClick={() => onOpenAnalytics(programmeId)}
className="min-tap-44"
>
<BarChart3 className="h-4 w-4 mr-2" />
Open Analytics
</Button>
<Dialog>
<DialogTrigger asChild>
<Button
variant="ghost"
size="icon"
className="min-tap-44"
aria-label="More actions"
>
<MoreHorizontal className="h-4 w-4" />
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle>Programme Actions</DialogTitle>
<DialogDescription>Additional actions for this programme</DialogDescription>
</DialogHeader>
<div className="space-y-2">
<Button variant="outline" className="w-full justify-start">
<FileText className="h-4 w-4 mr-2" />
View Structure JSON
</Button>
<Button variant="outline" className="w-full justify-start">
<ExternalLink className="h-4 w-4 mr-2" />
Audit Trail
</Button>
</div>
</DialogContent>
</Dialog>
</div>
</div>
</CardContent>
</Card>
{/* Summary Cards */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
<Card>
<CardContent className="pt-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-muted-foreground">Assignment Window</p>
<p className="font-semibold">{new Date(mockAssignment.startDate).toLocaleDateString()} {new Date(mockAssignment.endDate).toLocaleDateString()}</p>
<p className="text-xs text-muted-foreground">{mockAssignment.orgName}</p>
</div>
<Calendar className="h-8 w-8 text-brand-primary" />
</div>
</CardContent>
</Card>
<Card>
<CardContent className="pt-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-muted-foreground">Enrolled Learners</p>
<p className="text-2xl font-bold">{mockCounts.learners}</p>
<Button variant="link" className="p-0 h-auto text-xs">Manage</Button>
</div>
<Users className="h-8 w-8 text-brand-primary" />
</div>
</CardContent>
</Card>
<Card>
<CardContent className="pt-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-muted-foreground">Completion Rate</p>
<p className="text-2xl font-bold">{mockCounts.completionPct}%</p>
<Progress value={mockCounts.completionPct} className="w-16 mt-1" />
</div>
<Award className="h-8 w-8 text-brand-primary" />
</div>
</CardContent>
</Card>
<Card>
<CardContent className="pt-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-muted-foreground">Items in Programme</p>
<div className="flex gap-2 text-sm">
<span>{mockCounts.courses} Courses</span>
<span></span>
<span>{mockCounts.content} Content</span>
</div>
<div className="flex gap-2 text-sm text-muted-foreground">
<span>{mockCounts.webinars} Webinars</span>
<span></span>
<span>{mockCounts.classes} Classes</span>
</div>
</div>
<BookOpen className="h-8 w-8 text-brand-primary" />
</div>
</CardContent>
</Card>
</div>
{/* Tabs */}
<Card>
<CardContent className="pt-6">
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
<TabsList className="grid w-full grid-cols-6">
<TabsTrigger value="overview">Overview</TabsTrigger>
<TabsTrigger value="structure">Structure</TabsTrigger>
<TabsTrigger value="learners">Learners</TabsTrigger>
<TabsTrigger value="calendar">Calendar</TabsTrigger>
<TabsTrigger value="reports">Reports</TabsTrigger>
<TabsTrigger value="activity">Activity</TabsTrigger>
</TabsList>
{/* Overview Tab */}
<TabsContent value="overview" className="space-y-6 mt-6">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<Card>
<CardHeader>
<CardTitle>Programme Summary</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div>
<h4 className="font-medium mb-2">Description</h4>
<p className="text-sm text-muted-foreground">{mockProgramme.description}</p>
</div>
<div>
<h4 className="font-medium mb-2">Learning Goals</h4>
<ul className="text-sm text-muted-foreground space-y-1">
{mockProgramme.goals.map((goal, index) => (
<li key={index} className="flex items-start gap-2">
<CheckCircle className="h-4 w-4 text-status-success mt-0.5 flex-shrink-0" />
{goal}
</li>
))}
</ul>
</div>
<div>
<h4 className="font-medium mb-2">Tags</h4>
<div className="flex flex-wrap gap-2">
{mockProgramme.tags.map((tag, index) => (
<Badge key={index} variant="outline">{tag}</Badge>
))}
</div>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Stage Breakdown</CardTitle>
<CardDescription>Programme structure as designed by Super Admin</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-4">
{[
{ name: 'Pre-assessment', items: mockProgramme.structure.preAssessment, color: 'bg-chart-1' },
{ name: 'Pre-learning', items: mockProgramme.structure.preLearning, color: 'bg-chart-2' },
{ name: 'Classroom sessions', items: mockProgramme.structure.classroom, color: 'bg-chart-3' },
{ name: 'Post-learning', items: mockProgramme.structure.postLearning, color: 'bg-chart-4' }
].map((stage, index) => (
<div key={index} className="border-l-4 border-l-brand-primary pl-4">
<div className="flex items-center justify-between mb-2">
<h4 className="font-medium">{stage.name}</h4>
<Badge variant="outline">{stage.items.length} items</Badge>
</div>
<div className="space-y-1">
{stage.items.slice(0, 2).map((item, itemIndex) => (
<div key={itemIndex} className="flex items-center gap-2 text-sm">
{getTypeIcon(item.type)}
<span>{item.title}</span>
{item.status && getStatusBadge(item.status)}
</div>
))}
{stage.items.length > 2 && (
<p className="text-xs text-muted-foreground">
+{stage.items.length - 2} more items
</p>
)}
</div>
</div>
))}
</div>
</CardContent>
</Card>
</div>
<Alert>
<Bell className="h-4 w-4" />
<AlertDescription>
<strong>Guardrails:</strong> Dates and enrolments shown are scoped to your organization ({mockAssignment.orgName}).
</AlertDescription>
</Alert>
</TabsContent>
{/* Structure Tab */}
<TabsContent value="structure" className="space-y-6 mt-6">
<div className="space-y-4">
<div className="flex items-center justify-between">
<div>
<h3 className="text-lg font-semibold">Programme Structure</h3>
<p className="text-muted-foreground">Read-only view of the programme as composed by Super Admin</p>
</div>
<Badge variant="outline">Read Only</Badge>
</div>
<Accordion type="single" collapsible className="space-y-2">
{[
{ name: 'Pre-assessment', items: mockProgramme.structure.preAssessment },
{ name: 'Pre-learning', items: mockProgramme.structure.preLearning },
{ name: 'Classroom Sessions (Offline)', items: mockProgramme.structure.classroom },
{ name: 'Post-learning', items: mockProgramme.structure.postLearning }
].map((stage, index) => (
<AccordionItem key={index} value={`stage-${index}`} className="border rounded-lg px-4">
<AccordionTrigger className="hover:no-underline">
<div className="flex items-center gap-3">
<Badge variant="outline">{stage.name}</Badge>
<span className="text-muted-foreground">({stage.items.length} items)</span>
</div>
</AccordionTrigger>
<AccordionContent className="space-y-3 pb-4">
{stage.items.map((item, itemIndex) => (
<div key={itemIndex} className="flex items-center justify-between p-3 bg-muted/30 rounded-lg">
<div className="flex items-center gap-3">
{getTypeIcon(item.type)}
<div>
<p className="font-medium">{item.title}</p>
<div className="flex items-center gap-4 text-sm text-muted-foreground">
{item.duration && (
<span className="flex items-center gap-1">
<Clock className="h-3 w-3" />
{item.duration}
</span>
)}
{item.dueDate && (
<span className="flex items-center gap-1">
<Calendar className="h-3 w-3" />
Due: {item.dueDate}
</span>
)}
{item.venue && (
<span className="flex items-center gap-1">
<MapPin className="h-3 w-3" />
{item.venue} {item.room && `${item.room}`}
</span>
)}
{item.date && (
<span className="flex items-center gap-1">
<Calendar className="h-3 w-3" />
{item.date}
</span>
)}
</div>
</div>
</div>
<Button variant="outline" size="sm">
<Eye className="h-4 w-4 mr-2" />
Open
</Button>
</div>
))}
</AccordionContent>
</AccordionItem>
))}
</Accordion>
</div>
</TabsContent>
{/* Learners Tab */}
<TabsContent value="learners" className="space-y-6 mt-6">
<div className="flex flex-col sm:flex-row gap-4 items-start sm:items-center justify-between">
<div>
<h3 className="text-lg font-semibold">Learner Progress</h3>
<p className="text-muted-foreground">Track individual progress across the programme</p>
</div>
<div className="flex items-center gap-2">
<Button
variant="outline"
onClick={() => handleExport('excel')}
disabled={exporting}
className="min-tap-44"
>
{exporting ? (
<RefreshCw className="h-4 w-4 mr-2 animate-spin" />
) : (
<Download className="h-4 w-4 mr-2" />
)}
Export Data
</Button>
</div>
</div>
{/* Filters */}
<div className="flex flex-wrap gap-4 items-center">
<div className="relative flex-1 min-w-[200px] max-w-md">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
placeholder="Search learners..."
className="pl-10"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
</div>
<Select value={statusFilter} onValueChange={setStatusFilter}>
<SelectTrigger className="w-[150px]">
<SelectValue placeholder="Status" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Status</SelectItem>
<SelectItem value="Not Started">Not Started</SelectItem>
<SelectItem value="In-Progress">In Progress</SelectItem>
<SelectItem value="Completed">Completed</SelectItem>
</SelectContent>
</Select>
<Select value={stageFilter} onValueChange={setStageFilter}>
<SelectTrigger className="w-[150px]">
<SelectValue placeholder="Stage" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Stages</SelectItem>
<SelectItem value="Pre-assessment">Pre-assessment</SelectItem>
<SelectItem value="Pre-learning">Pre-learning</SelectItem>
<SelectItem value="Classroom">Classroom</SelectItem>
<SelectItem value="Post-learning">Post-learning</SelectItem>
</SelectContent>
</Select>
<Select value={cohortFilter} onValueChange={setCohortFilter}>
<SelectTrigger className="w-[150px]">
<SelectValue placeholder="Cohort" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Cohorts</SelectItem>
<SelectItem value="Cohort A">Cohort A</SelectItem>
<SelectItem value="Cohort B">Cohort B</SelectItem>
</SelectContent>
</Select>
</div>
{/* Learners Table */}
<div className="rounded-md border">
<Table>
<TableHeader className="sticky-header">
<TableRow>
<TableHead className="w-[200px]">Learner</TableHead>
<TableHead className="w-[200px]">Current Item</TableHead>
<TableHead className="w-[100px]">Progress</TableHead>
<TableHead className="w-[150px]">Next Item</TableHead>
<TableHead className="w-[120px]">Last Activity</TableHead>
<TableHead className="w-[100px]">Stage</TableHead>
<TableHead className="w-[80px]">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filteredLearners.map((learner) => (
<TableRow key={learner.id} className="min-h-[48px]">
<TableCell>
<div>
<p className="font-medium">{learner.name}</p>
<p className="text-sm text-muted-foreground">{learner.email}</p>
</div>
</TableCell>
<TableCell>
<div className="flex items-center gap-2">
{getTypeIcon(learner.currentItem.type)}
<div>
<p className="font-medium text-sm">{learner.currentItem.title}</p>
{getStatusBadge(learner.currentItem.status)}
</div>
</div>
</TableCell>
<TableCell>
<div className="space-y-1">
<Progress value={learner.progressPct} className="w-16" />
<span className="text-sm">{learner.progressPct}%</span>
</div>
</TableCell>
<TableCell>
{learner.nextItem ? (
<div className="flex items-center gap-2">
{getTypeIcon(learner.nextItem.type)}
<span className="text-sm">{learner.nextItem.title}</span>
</div>
) : (
<span className="text-sm text-muted-foreground">None</span>
)}
</TableCell>
<TableCell className="text-sm text-muted-foreground">
{learner.lastActivity}
</TableCell>
<TableCell>
<Badge variant="outline" className="text-xs">
{learner.stage}
</Badge>
</TableCell>
<TableCell>
<div className="flex gap-1">
<Button
variant="ghost"
size="sm"
onClick={() => handleViewLearner(learner)}
className="min-tap-44"
aria-label={`View details for ${learner.name}`}
>
<Eye className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="sm"
className="min-tap-44"
aria-label={`Send reminder to ${learner.name}`}
>
<Mail className="h-4 w-4" />
</Button>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
</TabsContent>
{/* Calendar Tab */}
<TabsContent value="calendar" className="space-y-6 mt-6">
<div className="text-center py-12 text-muted-foreground">
<Calendar className="h-12 w-12 mx-auto mb-4" />
<h3 className="text-lg font-semibold mb-2">Programme Calendar</h3>
<p>Programme-scoped calendar with webinars, offline classes, and deadlines would be displayed here</p>
</div>
</TabsContent>
{/* Reports Tab */}
<TabsContent value="reports" className="space-y-6 mt-6">
<div className="text-center py-12 text-muted-foreground">
<BarChart3 className="h-12 w-12 mx-auto mb-4" />
<h3 className="text-lg font-semibold mb-2">Programme Reports</h3>
<p>Detailed analytics and reporting for this programme would be displayed here</p>
</div>
</TabsContent>
{/* Activity Tab */}
<TabsContent value="activity" className="space-y-6 mt-6">
<div className="text-center py-12 text-muted-foreground">
<Activity className="h-12 w-12 mx-auto mb-4" />
<h3 className="text-lg font-semibold mb-2">Activity Log</h3>
<p>Programme activity audit trail would be displayed here</p>
</div>
</TabsContent>
</Tabs>
</CardContent>
</Card>
{/* Learner Details Drawer */}
<Sheet open={showLearnerDrawer} onOpenChange={setShowLearnerDrawer}>
<SheetContent className="w-[480px] sm:w-[540px]">
<SheetHeader>
<SheetTitle>{selectedLearner?.name}</SheetTitle>
<SheetDescription>Learner progress and assignment details</SheetDescription>
</SheetHeader>
{selectedLearner && (
<div className="space-y-6 mt-6">
<div>
<h4 className="font-medium mb-2">Assignment Info</h4>
<div className="space-y-2 text-sm">
<div className="flex justify-between">
<span className="text-muted-foreground">Start Date:</span>
<span>{mockAssignment.startDate}</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">End Date:</span>
<span>{mockAssignment.endDate}</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">Cohort:</span>
<span>{selectedLearner.cohort}</span>
</div>
</div>
</div>
<div>
<h4 className="font-medium mb-2">Stage Timeline</h4>
<div className="space-y-3">
{[
{ name: 'Pre-assessment', completed: selectedLearner.stage !== 'Pre-assessment' },
{ name: 'Pre-learning', completed: !['Pre-assessment', 'Pre-learning'].includes(selectedLearner.stage) },
{ name: 'Classroom', completed: !['Pre-assessment', 'Pre-learning', 'Classroom'].includes(selectedLearner.stage) },
{ name: 'Post-learning', completed: false }
].map((stage, index) => (
<div key={index} className="flex items-center gap-3">
<div className={`w-4 h-4 rounded-full border-2 ${
stage.completed ? 'bg-status-success border-status-success' :
selectedLearner.stage === stage.name ? 'border-brand-primary bg-brand-primary' :
'border-muted'
}`}>
{stage.completed && <CheckCircle className="w-4 h-4 text-white" />}
</div>
<span className={`text-sm ${
selectedLearner.stage === stage.name ? 'font-medium' : 'text-muted-foreground'
}`}>
{stage.name}
</span>
</div>
))}
</div>
</div>
<div className="flex gap-2">
<Button variant="outline" className="flex-1">
<Eye className="h-4 w-4 mr-2" />
Open Learner Record
</Button>
<Button variant="outline" className="flex-1">
<Mail className="h-4 w-4 mr-2" />
Send Reminder
</Button>
</div>
</div>
)}
</SheetContent>
</Sheet>
</div>
);
};

View File

@@ -0,0 +1,367 @@
import React, { useState } from 'react';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from './ui/card';
import { Badge } from './ui/badge';
import { Button } from './ui/button';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './ui/select';
import {
Calendar,
Clock,
Users,
Video,
FileText,
ChevronLeft,
ChevronRight,
Filter
} from 'lucide-react';
interface ScheduleEvent {
id: string;
title: string;
type: 'webinar' | 'workshop' | 'assessment' | 'deadline' | 'class';
date: Date;
time: string;
duration?: string;
programme: string;
attendees?: number;
maxAttendees?: number;
status: 'upcoming' | 'live' | 'completed';
}
interface ProgrammeScheduleProps {
events?: ScheduleEvent[];
onEventClick?: (event: ScheduleEvent) => void;
}
const mockEvents: ScheduleEvent[] = [
{
id: 'evt-001',
title: 'Leadership Fundamentals Webinar',
type: 'webinar',
date: new Date('2024-12-28'),
time: '10:00 AM',
duration: '90 min',
programme: 'Leadership Development',
attendees: 32,
maxAttendees: 50,
status: 'upcoming'
},
{
id: 'evt-002',
title: 'Technical Skills Assessment',
type: 'assessment',
date: new Date('2024-12-28'),
time: '2:00 PM',
duration: '60 min',
programme: 'Technical Skills',
status: 'upcoming'
},
{
id: 'evt-003',
title: 'Communication Workshop',
type: 'workshop',
date: new Date('2024-12-29'),
time: '11:00 AM',
duration: '2 hrs',
programme: 'Communication Excellence',
attendees: 18,
maxAttendees: 25,
status: 'upcoming'
},
{
id: 'evt-004',
title: 'Project Management Live Class',
type: 'class',
date: new Date('2024-12-29'),
time: '3:30 PM',
duration: '45 min',
programme: 'Project Management',
attendees: 45,
maxAttendees: 60,
status: 'live'
},
{
id: 'evt-005',
title: 'Assignment Submission Due',
type: 'deadline',
date: new Date('2024-12-30'),
time: '11:59 PM',
programme: 'Leadership Development',
status: 'upcoming'
},
{
id: 'evt-006',
title: 'Data Analytics Bootcamp',
type: 'workshop',
date: new Date('2024-12-30'),
time: '9:00 AM',
duration: '4 hrs',
programme: 'Technical Skills',
attendees: 22,
maxAttendees: 30,
status: 'upcoming'
}
];
export const ProgrammeSchedule: React.FC<ProgrammeScheduleProps> = ({
events = mockEvents,
onEventClick
}) => {
const [selectedProgramme, setSelectedProgramme] = useState('all');
const [selectedType, setSelectedType] = useState('all');
const [currentWeekStart, setCurrentWeekStart] = useState(() => {
const today = new Date();
const dayOfWeek = today.getDay();
const mondayDate = new Date(today);
mondayDate.setDate(today.getDate() - dayOfWeek + 1);
return mondayDate;
});
// Get unique programmes and types for filters
const programmes = Array.from(new Set(events.map(e => e.programme)));
const eventTypes = Array.from(new Set(events.map(e => e.type)));
// Generate week days
const weekDays = Array.from({ length: 7 }, (_, i) => {
const date = new Date(currentWeekStart);
date.setDate(currentWeekStart.getDate() + i);
return date;
});
// Filter and group events by date
const filteredEvents = events.filter(event => {
const matchesProgramme = selectedProgramme === 'all' || event.programme === selectedProgramme;
const matchesType = selectedType === 'all' || event.type === selectedType;
return matchesProgramme && matchesType;
});
const eventsByDate = weekDays.reduce((acc, date) => {
const dateKey = date.toDateString();
acc[dateKey] = filteredEvents.filter(event =>
event.date.toDateString() === dateKey
).sort((a, b) => a.time.localeCompare(b.time));
return acc;
}, {} as Record<string, ScheduleEvent[]>);
const navigateWeek = (direction: 'prev' | 'next') => {
const newDate = new Date(currentWeekStart);
newDate.setDate(currentWeekStart.getDate() + (direction === 'next' ? 7 : -7));
setCurrentWeekStart(newDate);
};
const getEventIcon = (type: ScheduleEvent['type']) => {
switch (type) {
case 'webinar':
return <Video className="h-3 w-3" />;
case 'workshop':
return <Users className="h-3 w-3" />;
case 'assessment':
return <FileText className="h-3 w-3" />;
case 'deadline':
return <Clock className="h-3 w-3" />;
case 'class':
return <Calendar className="h-3 w-3" />;
default:
return <Calendar className="h-3 w-3" />;
}
};
const getEventColor = (type: ScheduleEvent['type'], status: ScheduleEvent['status']) => {
if (status === 'live') return 'bg-status-error text-status-error-foreground';
if (status === 'completed') return 'bg-muted text-muted-foreground';
switch (type) {
case 'webinar':
return 'bg-brand-primary text-brand-navy-foreground';
case 'workshop':
return 'bg-status-success text-status-success-foreground';
case 'assessment':
return 'bg-status-warn text-status-warn-foreground';
case 'deadline':
return 'bg-status-error text-status-error-foreground';
case 'class':
return 'bg-brand-charcoal text-brand-charcoal-foreground';
default:
return 'bg-secondary text-secondary-foreground';
}
};
const formatDate = (date: Date) => {
return date.toLocaleDateString('en-AU', {
weekday: 'short',
day: '2-digit',
month: 'short'
});
};
const isToday = (date: Date) => {
const today = new Date();
return date.toDateString() === today.toDateString();
};
return (
<Card>
<CardHeader>
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div>
<CardTitle>Programme Schedule</CardTitle>
<CardDescription>Weekly view of upcoming classes, webinars, and deadlines</CardDescription>
</div>
<div className="flex items-center gap-2">
<Select value={selectedProgramme} onValueChange={setSelectedProgramme}>
<SelectTrigger className="w-[180px]">
<Filter className="h-4 w-4 mr-2" />
<SelectValue placeholder="All Programmes" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Programmes</SelectItem>
{programmes.map(programme => (
<SelectItem key={programme} value={programme}>
{programme}
</SelectItem>
))}
</SelectContent>
</Select>
<Select value={selectedType} onValueChange={setSelectedType}>
<SelectTrigger className="w-[140px]">
<SelectValue placeholder="All Types" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Types</SelectItem>
{eventTypes.map(type => (
<SelectItem key={type} value={type}>
{type.charAt(0).toUpperCase() + type.slice(1)}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
</CardHeader>
<CardContent>
{/* Week Navigation */}
<div className="flex items-center justify-between mb-6">
<Button
variant="outline"
size="sm"
onClick={() => navigateWeek('prev')}
className="min-tap-44"
>
<ChevronLeft className="h-4 w-4 mr-1" />
Previous Week
</Button>
<div className="text-center">
<h3 className="font-semibold">
{weekDays[0].toLocaleDateString('en-AU', { day: '2-digit', month: 'short' })} - {weekDays[6].toLocaleDateString('en-AU', { day: '2-digit', month: 'short', year: 'numeric' })}
</h3>
<p className="text-sm text-muted-foreground">Week View</p>
</div>
<Button
variant="outline"
size="sm"
onClick={() => navigateWeek('next')}
className="min-tap-44"
>
Next Week
<ChevronRight className="h-4 w-4 ml-1" />
</Button>
</div>
{/* Horizontal Weekly Calendar */}
<div className="grid grid-cols-7 gap-2">
{weekDays.map((date, dayIndex) => {
const dateKey = date.toDateString();
const dayEvents = eventsByDate[dateKey] || [];
return (
<div
key={dayIndex}
className={`
min-h-[120px] p-3 rounded-lg border
${isToday(date)
? 'bg-brand-primary/5 border-brand-primary/20'
: 'bg-card border-chrome-divider'
}
`}
>
<div className="text-center mb-3">
<p className={`font-medium ${isToday(date) ? 'text-brand-primary' : ''}`}>
{formatDate(date)}
</p>
{isToday(date) && (
<Badge variant="secondary" className="text-xs mt-1">Today</Badge>
)}
</div>
<div className="space-y-1">
{dayEvents.slice(0, 3).map((event) => (
<button
key={event.id}
onClick={() => onEventClick?.(event)}
className={`
w-full p-2 rounded text-left text-xs transition-all duration-200
hover:shadow-sm hover:scale-105 min-tap-44
${getEventColor(event.type, event.status)}
`}
>
<div className="flex items-center gap-1 mb-1">
{getEventIcon(event.type)}
<span className="font-medium truncate">{event.title}</span>
</div>
<div className="flex items-center justify-between">
<span>{event.time}</span>
{event.status === 'live' && (
<Badge variant="destructive" className="text-xs px-1 py-0">
LIVE
</Badge>
)}
</div>
{event.attendees && (
<div className="flex items-center gap-1 mt-1 text-xs opacity-80">
<Users className="h-2 w-2" />
<span>{event.attendees}/{event.maxAttendees}</span>
</div>
)}
</button>
))}
{dayEvents.length > 3 && (
<div className="text-center">
<span className="text-xs text-muted-foreground">
+{dayEvents.length - 3} more
</span>
</div>
)}
{dayEvents.length === 0 && (
<div className="text-center py-4">
<span className="text-xs text-muted-foreground">No events</span>
</div>
)}
</div>
</div>
);
})}
</div>
{/* Legend */}
<div className="mt-6 pt-4 border-t border-chrome-divider">
<p className="text-sm font-medium mb-2">Event Types:</p>
<div className="flex flex-wrap gap-2">
{[
{ type: 'webinar', label: 'Webinar' },
{ type: 'workshop', label: 'Workshop' },
{ type: 'class', label: 'Live Class' },
{ type: 'assessment', label: 'Assessment' },
{ type: 'deadline', label: 'Deadline' }
].map(({ type, label }) => (
<div key={type} className="flex items-center gap-1">
<div className={`w-3 h-3 rounded ${getEventColor(type as ScheduleEvent['type'], 'upcoming').replace('text-', 'bg-').split(' ')[0]}`} />
<span className="text-xs text-muted-foreground">{label}</span>
</div>
))}
</div>
</div>
</CardContent>
</Card>
);
};

View File

@@ -0,0 +1,485 @@
import React, { useState } from 'react';
import { Button } from './ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from './ui/card';
import { Badge } from './ui/badge';
import { Input } from './ui/input';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './ui/select';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from './ui/table';
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from './ui/dialog';
import {
Search,
Filter,
Download,
Users,
Calendar,
BookOpen,
RefreshCw,
Eye,
UserPlus,
ChevronLeft,
ChevronRight
} from 'lucide-react';
interface Assignment {
startDate: Date;
endDate: Date;
}
interface Programme {
programmeId: string;
title: string;
status: 'Active' | 'Upcoming' | 'Completed';
coursesCount: number;
contentCount: number;
assignment: Assignment;
learnersAssigned: number;
type?: 'programme' | 'course';
}
interface ProgrammesTableProps {
programmes?: Programme[];
onViewProgramme?: (programmeId: string) => void;
onViewCourse?: (courseId: string) => void;
onAssignLearners?: (programmeId: string) => void;
onDownloadTracker?: (programmeId: string) => void;
userAccessLevel?: 'full' | 'course-only'; // New prop to determine user access
}
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,
type: 'programme'
},
{
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,
type: 'programme'
},
{
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,
type: 'programme'
},
{
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,
type: 'programme'
},
{
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,
type: 'programme'
},
{
programmeId: 'prog-006',
title: 'Data Analytics Fundamentals',
status: 'Completed',
coursesCount: 7,
contentCount: 21,
assignment: {
startDate: new Date('2023-10-15'),
endDate: new Date('2024-01-15')
},
learnersAssigned: 29,
type: 'programme'
}
];
const mockCourses: Programme[] = [
{
programmeId: 'course-001',
title: 'Strategic Thinking Course',
status: 'Active',
coursesCount: 1,
contentCount: 8,
assignment: {
startDate: new Date('2024-01-15'),
endDate: new Date('2024-03-15')
},
learnersAssigned: 15,
type: 'course'
},
{
programmeId: 'course-002',
title: 'Data Analysis Fundamentals',
status: 'Active',
coursesCount: 1,
contentCount: 12,
assignment: {
startDate: new Date('2024-02-01'),
endDate: new Date('2024-04-30')
},
learnersAssigned: 22,
type: 'course'
},
{
programmeId: 'course-003',
title: 'Public Speaking Mastery',
status: 'Upcoming',
coursesCount: 1,
contentCount: 6,
assignment: {
startDate: new Date('2024-03-01'),
endDate: new Date('2024-04-15')
},
learnersAssigned: 18,
type: 'course'
},
{
programmeId: 'course-004',
title: 'Agile Methodology Workshop',
status: 'Active',
coursesCount: 1,
contentCount: 10,
assignment: {
startDate: new Date('2024-01-01'),
endDate: new Date('2024-06-30')
},
learnersAssigned: 25,
type: 'course'
},
{
programmeId: 'course-005',
title: 'Excel Advanced Techniques',
status: 'Completed',
coursesCount: 1,
contentCount: 5,
assignment: {
startDate: new Date('2023-11-01'),
endDate: new Date('2023-12-31')
},
learnersAssigned: 12,
type: 'course'
}
];
export const ProgrammesTable: React.FC<ProgrammesTableProps> = ({
programmes,
onViewProgramme,
onViewCourse,
onAssignLearners,
onDownloadTracker,
userAccessLevel = 'full'
}) => {
const [searchTerm, setSearchTerm] = useState('');
const [statusFilter, setStatusFilter] = useState('Active');
const [currentPage, setCurrentPage] = useState(1);
const [isAssignModalOpen, setIsAssignModalOpen] = useState(false);
const [selectedProgramme, setSelectedProgramme] = useState<Programme | null>(null);
const [isExporting, setIsExporting] = useState(false);
const itemsPerPage = 5;
// Determine which data to show based on user access level
const defaultData = userAccessLevel === 'course-only' ? mockCourses : mockProgrammes;
const displayData = programmes || defaultData;
const filteredProgrammes = displayData.filter(prog => {
const matchesSearch = prog.title.toLowerCase().includes(searchTerm.toLowerCase());
const matchesStatus = statusFilter === 'all' || prog.status === statusFilter;
return matchesSearch && matchesStatus;
});
// Pagination logic
const totalPages = Math.ceil(filteredProgrammes.length / itemsPerPage);
const startIndex = (currentPage - 1) * itemsPerPage;
const endIndex = startIndex + itemsPerPage;
const paginatedProgrammes = filteredProgrammes.slice(startIndex, endIndex);
// Reset to first page when filters change
React.useEffect(() => {
setCurrentPage(1);
}, [searchTerm, statusFilter]);
const formatDateRange = (assignment: Assignment) => {
const startDate = assignment.startDate.toLocaleDateString('en-AU', {
day: '2-digit',
month: 'short',
year: 'numeric'
});
const endDate = assignment.endDate.toLocaleDateString('en-AU', {
day: '2-digit',
month: 'short',
year: 'numeric'
});
return `${startDate}${endDate}`;
};
const handleViewItem = (item: Programme) => {
if (item.type === 'course') {
onViewCourse?.(item.programmeId);
console.log(`Viewing course: ${item.programmeId}`);
} else {
onViewProgramme?.(item.programmeId);
console.log(`Viewing programme: ${item.programmeId}`);
}
};
const handleAssignLearners = (programme: Programme) => {
setSelectedProgramme(programme);
setIsAssignModalOpen(true);
onAssignLearners?.(programme.programmeId);
};
const handleDownloadTracker = async (programmeId: string) => {
setIsExporting(true);
await new Promise(resolve => setTimeout(resolve, 2000));
setIsExporting(false);
onDownloadTracker?.(programmeId);
console.log(`Downloaded tracker for programme: ${programmeId}`);
};
const getStatusBadgeProps = (status: Programme['status']) => {
switch (status) {
case 'Active':
return { variant: 'default' as const, className: 'bg-status-success text-status-success-foreground' };
case 'Upcoming':
return { variant: 'secondary' as const, className: 'bg-status-warn text-status-warn-foreground' };
case 'Completed':
return { variant: 'outline' as const, className: 'bg-muted text-muted-foreground' };
default:
return { variant: 'secondary' as const };
}
};
return (
<Card>
<CardHeader>
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div>
<CardTitle>{userAccessLevel === 'course-only' ? 'Courses' : 'Programmes'}</CardTitle>
<CardDescription>
{userAccessLevel === 'course-only'
? 'Manage course assignments and track progress'
: 'Manage programme assignments and track progress'
}
</CardDescription>
</div>
<div className="flex items-center gap-2">
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
placeholder={userAccessLevel === 'course-only' ? 'Search courses...' : 'Search programmes...'}
className="pl-10 w-[200px]"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
aria-label={userAccessLevel === 'course-only' ? 'Search courses by title' : 'Search programmes by title'}
/>
</div>
<Select value={statusFilter} onValueChange={setStatusFilter}>
<SelectTrigger className="w-[140px]">
<Filter className="h-4 w-4 mr-2" />
<SelectValue placeholder="Status" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Status</SelectItem>
<SelectItem value="Active">Active</SelectItem>
<SelectItem value="Upcoming">Upcoming</SelectItem>
<SelectItem value="Completed">Completed</SelectItem>
</SelectContent>
</Select>
</div>
</div>
</CardHeader>
<CardContent>
<div className="rounded-md border" style={{ maxWidth: '1200px' }}>
<Table>
<TableHeader className="sticky-header">
<TableRow>
<TableHead className="w-[300px]">
{userAccessLevel === 'course-only' ? 'Course' : 'Programme/Course'}
</TableHead>
<TableHead className="w-[150px]">
{userAccessLevel === 'course-only' ? 'Content' : 'Courses / Content'}
</TableHead>
<TableHead className="w-[200px]">Start End</TableHead>
<TableHead className="w-[120px]">Learners Assigned</TableHead>
<TableHead className="w-[200px]">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{paginatedProgrammes.map((programme) => (
<TableRow key={programme.programmeId} className="min-h-[44px]">
<TableCell>
<div className="flex items-center gap-2">
<span className="font-medium">{programme.title}</span>
<Badge
{...getStatusBadgeProps(programme.status)}
aria-label={`Programme status: ${programme.status}`}
>
{programme.status}
</Badge>
</div>
</TableCell>
<TableCell>
<div className="flex items-center gap-1 text-sm text-muted-foreground">
<BookOpen className="h-4 w-4" />
<span>
{userAccessLevel === 'course-only'
? `${programme.contentCount} modules`
: `${programme.coursesCount}${programme.contentCount}`
}
</span>
</div>
</TableCell>
<TableCell className="text-sm">
{formatDateRange(programme.assignment)}
</TableCell>
<TableCell>
<div className="flex items-center gap-1">
<Users className="h-4 w-4 text-muted-foreground" />
<span className="font-medium">{programme.learnersAssigned}</span>
</div>
</TableCell>
<TableCell>
<div className="flex items-center gap-1">
<Button
variant="ghost"
size="sm"
onClick={() => handleViewItem(programme)}
className="min-tap-44"
aria-label={`View ${programme.type === 'course' ? 'course' : 'programme'} details for ${programme.title}`}
>
<Eye className="h-4 w-4" />
<span className="sr-only">View {programme.type === 'course' ? 'Course' : 'Programme'}</span>
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => handleAssignLearners(programme)}
className="min-tap-44"
aria-label={`Assign learners to ${programme.title}`}
>
<UserPlus className="h-4 w-4" />
<span className="sr-only">Assign Learners</span>
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => handleDownloadTracker(programme.programmeId)}
disabled={isExporting}
className="min-tap-44"
aria-label={`Download tracker for ${programme.title}`}
>
{isExporting ? (
<RefreshCw className="h-4 w-4 animate-spin" />
) : (
<Download className="h-4 w-4" />
)}
<span className="sr-only">Download Tracker</span>
</Button>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
{/* Pagination */}
{totalPages > 1 && (
<div className="flex items-center justify-between mt-4">
<div className="text-sm text-muted-foreground">
Showing {startIndex + 1} to {Math.min(endIndex, filteredProgrammes.length)} of {filteredProgrammes.length} {userAccessLevel === 'course-only' ? 'courses' : 'programmes'}
</div>
<div className="flex items-center gap-2">
<Button
variant="outline"
size="sm"
onClick={() => setCurrentPage(prev => Math.max(1, prev - 1))}
disabled={currentPage === 1}
className="min-tap-44"
>
<ChevronLeft className="h-4 w-4 mr-1" />
Previous
</Button>
<span className="text-sm text-muted-foreground">
Page {currentPage} of {totalPages}
</span>
<Button
variant="outline"
size="sm"
onClick={() => setCurrentPage(prev => Math.min(totalPages, prev + 1))}
disabled={currentPage === totalPages}
className="min-tap-44"
>
Next
<ChevronRight className="h-4 w-4 ml-1" />
</Button>
</div>
</div>
)}
{filteredProgrammes.length === 0 && (
<div className="text-center py-8">
<p className="text-muted-foreground">
No {userAccessLevel === 'course-only' ? 'courses' : 'programmes'} found matching your criteria.
</p>
</div>
)}
{/* Assignment Modal */}
<Dialog open={isAssignModalOpen} onOpenChange={setIsAssignModalOpen}>
<DialogContent className="sm:max-w-[600px]" role="dialog" aria-modal="true">
<DialogHeader>
<DialogTitle>
Assign Learners to {userAccessLevel === 'course-only' ? 'Course' : 'Programme'}
</DialogTitle>
<DialogDescription>
{selectedProgramme && `Assign learners to "${selectedProgramme.title}"`}
</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-4">
<div className="text-center py-8 text-muted-foreground">
<UserPlus className="h-12 w-12 mx-auto mb-4 text-muted-foreground" />
<p>Assignment wizard would be displayed here</p>
<p className="text-sm mt-2">Including org/individual selection, dates, HR contacts, and participant upload</p>
</div>
</div>
</DialogContent>
</Dialog>
</CardContent>
</Card>
);
};

View File

@@ -34,25 +34,25 @@ const buttonVariants = cva(
},
);
function Button({
className,
variant,
size,
asChild = false,
...props
}: React.ComponentProps<"button"> &
VariantProps<typeof buttonVariants> & {
asChild?: boolean;
}) {
const Button = React.forwardRef<
HTMLButtonElement,
React.ComponentProps<"button"> &
VariantProps<typeof buttonVariants> & {
asChild?: boolean;
}
>(({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button";
return (
<Comp
data-slot="button"
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
);
}
});
Button.displayName = "Button";
export { Button, buttonVariants };

25
src/global.d.ts vendored
View File

@@ -1,26 +1,29 @@
// declarations.d.ts
declare module "*.png" {
declare module '*.webp' {
const src: string;
export default src;
}
declare module "*.jpg" {
declare module '*.png' {
const src: string;
export default src;
}
declare module "*.jpeg" {
declare module '*.jpg' {
const src: string;
export default src;
}
declare module "*.svg" {
import * as React from "react";
const ReactComponent: React.FunctionComponent<
React.SVGProps<SVGSVGElement> & { title?: string }
>;
export { ReactComponent };
declare module '*.jpeg' {
const src: string;
export default src;
}
declare module '*.svg' {
const src: string;
export default src;
}
declare module '*.gif' {
const src: string;
export default src;
}

3967
src/index.css Normal file

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -1,5 +1,5 @@
@import url("https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap");
@import "tailwindcss";
@custom-variant dark (&:is(.dark *));
:root {
@@ -500,8 +500,8 @@
/* Utility classes for HR Portal */
@layer utilities {
.min-tap-44 {
min-height: 44px;
min-width: 44px;
min-height: 12px;
min-width: 12px;
}
.sticky-header {

View File

@@ -1,7 +1,7 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react-swc';
import path from 'path';
import * as path from 'path';
export default defineConfig({
plugins: [react()],
@@ -54,7 +54,7 @@
outDir: 'build',
},
server: {
port: 3000,
port: 3005,
open: true,
},
});