196 lines
7.5 KiB
TypeScript
196 lines
7.5 KiB
TypeScript
import React, { useEffect } from 'react';
|
|
import { useParams, Link, useNavigate } from 'react-router-dom';
|
|
import { AccessTime, Add, ArrowBack, CalendarToday, Delete, Edit, KeyboardArrowLeft, KeyboardArrowRight, LocationOn } from '@mui/icons-material';
|
|
import useTimetableStore from '../../stores/timetableStore';
|
|
import { useUser } from '../../contexts/UserContext';
|
|
import { format, parseISO, addDays, startOfWeek, isSameDay } from 'date-fns';
|
|
|
|
const TimetablePage: React.FC = () => {
|
|
const { timetableId } = useParams<{ timetableId: string }>();
|
|
const navigate = useNavigate();
|
|
const { profile } = useUser();
|
|
const {
|
|
currentTimetable,
|
|
currentLessons,
|
|
timetableDetailLoading,
|
|
timetableDetailError,
|
|
fetchTimetable,
|
|
deleteTimetable,
|
|
clearCurrentTimetable,
|
|
} = useTimetableStore();
|
|
|
|
useEffect(() => {
|
|
if (timetableId) {
|
|
fetchTimetable(timetableId);
|
|
}
|
|
return () => {
|
|
clearCurrentTimetable();
|
|
};
|
|
}, [timetableId, fetchTimetable, clearCurrentTimetable]);
|
|
|
|
const handleDeleteTimetable = async () => {
|
|
if (!timetableId) return;
|
|
if (confirm('Are you sure you want to delete this timetable?')) {
|
|
await deleteTimetable(timetableId);
|
|
navigate(`/timetable/classes/${currentTimetable?.class_id}`);
|
|
}
|
|
};
|
|
|
|
// Group lessons by day
|
|
const lessonsByDay = (currentLessons || []).reduce((acc, lesson) => {
|
|
const date = lesson.day_of_week || format(parseISO(lesson.start_time), 'yyyy-MM-dd');
|
|
if (!acc[date]) acc[date] = [];
|
|
acc[date].push(lesson);
|
|
return acc;
|
|
}, {} as Record<string, typeof currentLessons>);
|
|
|
|
// Sort lessons within each day by start time
|
|
Object.keys(lessonsByDay).forEach(day => {
|
|
lessonsByDay[day].sort((a, b) =>
|
|
new Date(a.start_time).getTime() - new Date(b.start_time).getTime()
|
|
);
|
|
});
|
|
|
|
if (timetableDetailLoading) {
|
|
return (
|
|
<div className="flex justify-center items-center h-64">
|
|
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600"></div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (timetableDetailError || !currentTimetable) {
|
|
return (
|
|
<div className="container mx-auto px-4 py-6">
|
|
<div className="bg-red-50 border border-red-200 rounded-xl p-6">
|
|
<h2 className="text-lg font-semibold text-red-800 mb-2">Error Loading Timetable</h2>
|
|
<p className="text-red-600">{timetableDetailError || 'Timetable not found'}</p>
|
|
<Link to="/timetable/classes" className="text-blue-600 hover:underline mt-4 inline-block">
|
|
Back to Classes
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="container mx-auto px-4 py-6 max-w-6xl">
|
|
{/* Header */}
|
|
<div className="mb-6">
|
|
<Link
|
|
to={`/timetable/classes/${currentTimetable.class_id}`}
|
|
className="inline-flex items-center gap-2 text-gray-500 hover:text-gray-700 mb-4"
|
|
>
|
|
<ArrowBack sx={{ fontSize: 18 }} />
|
|
Back to Class
|
|
</Link>
|
|
|
|
<div className="flex flex-col md:flex-row md:items-start md:justify-between gap-4">
|
|
<div>
|
|
<h1 className="text-3xl font-bold text-gray-900 mb-2">{currentTimetable.name}</h1>
|
|
<div className="flex items-center gap-4 text-gray-500">
|
|
<span className="flex items-center gap-1">
|
|
<CalendarToday sx={{ fontSize: 16 }} />
|
|
{format(parseISO(currentTimetable.effective_from), 'MMM d, yyyy')}
|
|
{currentTimetable.effective_until && ` - ${format(parseISO(currentTimetable.effective_until), 'MMM d, yyyy')}`}
|
|
</span>
|
|
{currentTimetable.is_recurring && (
|
|
<span className="px-2 py-1 bg-green-100 text-green-700 text-xs font-medium rounded-full">
|
|
Recurring
|
|
</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-2">
|
|
<Link
|
|
to={`/timetable/timetables/${timetableId}/lessons/new`}
|
|
className="inline-flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
|
|
>
|
|
<Add sx={{ fontSize: 18 }} />
|
|
Add Lesson
|
|
</Link>
|
|
<button
|
|
onClick={handleDeleteTimetable}
|
|
className="inline-flex items-center gap-2 px-4 py-2 bg-red-100 text-red-700 rounded-lg hover:bg-red-200 transition-colors"
|
|
>
|
|
<Delete sx={{ fontSize: 18 }} />
|
|
Delete
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Lessons List */}
|
|
<div className="bg-white rounded-xl shadow-sm border border-gray-200">
|
|
<div className="p-6 border-b border-gray-200">
|
|
<h2 className="text-lg font-semibold text-gray-900">
|
|
Lessons ({currentLessons.length})
|
|
</h2>
|
|
</div>
|
|
|
|
{currentLessons.length === 0 ? (
|
|
<div className="p-12 text-center text-gray-500">
|
|
<AccessTime sx={{ fontSize: 48 }} className="mx-auto mb-4 opacity-50" />
|
|
<p className="text-lg font-medium mb-2">No lessons scheduled</p>
|
|
<p>Add lessons to build your timetable</p>
|
|
</div>
|
|
) : (
|
|
<div className="divide-y divide-gray-200">
|
|
{Object.entries(lessonsByDay).map(([day, lessons]) => (
|
|
<div key={day} className="p-6">
|
|
<h3 className="text-sm font-medium text-gray-500 uppercase tracking-wide mb-4">
|
|
{currentTimetable.is_recurring
|
|
? `Day ${day}`
|
|
: format(parseISO(day), 'EEEE, MMMM d, yyyy')}
|
|
</h3>
|
|
<div className="space-y-3">
|
|
{lessons.map((lesson) => (
|
|
<Link
|
|
key={lesson.id}
|
|
to={`/timetable/lessons/${lesson.id}`}
|
|
className="flex items-center gap-4 p-4 bg-gray-50 rounded-lg hover:bg-gray-100 transition-colors"
|
|
>
|
|
<div className="flex-shrink-0 w-16 text-center">
|
|
<div className="text-sm font-medium text-gray-900">
|
|
{format(parseISO(lesson.start_time), 'HH:mm')}
|
|
</div>
|
|
<div className="text-xs text-gray-500">
|
|
{format(parseISO(lesson.end_time), 'HH:mm')}
|
|
</div>
|
|
</div>
|
|
<div className="flex-1 min-w-0">
|
|
<h4 className="font-medium text-gray-900 truncate">
|
|
{lesson.title}
|
|
</h4>
|
|
{lesson.description && (
|
|
<p className="text-sm text-gray-500 truncate">
|
|
{lesson.description}
|
|
</p>
|
|
)}
|
|
</div>
|
|
{lesson.location && (
|
|
<div className="flex items-center gap-1 text-sm text-gray-500">
|
|
<LocationOn sx={{ fontSize: 14 }} />
|
|
{lesson.location}
|
|
</div>
|
|
)}
|
|
{lesson.room && (
|
|
<span className="px-2 py-1 bg-blue-100 text-blue-700 text-xs font-medium rounded">
|
|
Room {lesson.room}
|
|
</span>
|
|
)}
|
|
</Link>
|
|
))}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default TimetablePage;
|