Files
KLC-Hr-Dashboard-Frontend/src/components/ProgrammeSchedule.tsx
2025-09-26 16:37:17 +05:30

367 lines
12 KiB
TypeScript

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>
);
};