902 lines
37 KiB
TypeScript
902 lines
37 KiB
TypeScript
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>
|
|
);
|
|
}; |