502 lines
18 KiB
TypeScript
502 lines
18 KiB
TypeScript
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>
|
|
);
|
|
}; |