- Fix icon naming: remove 'Icon' suffix from MUI icon components in Header.tsx (AccessTime, Close, Person, School, Schedule, Class, Book, Settings, Student, Login, Logout) - Update timetable components to use UserContext instead of ProfileContext - Fix timetableService naming collision and circular reference - Update various components for consistency
294 lines
11 KiB
TypeScript
294 lines
11 KiB
TypeScript
import React, { useEffect, useState } from 'react';
|
|
import { useParams, Link, useNavigate } from 'react-router-dom';
|
|
import { AccessTime, ArrowBack, CalendarToday, Cancel, CheckCircle, Delete, Description, Edit, LocationOn, MenuBook, People } from '@mui/icons-material';
|
|
import useTimetableStore from '../../stores/timetableStore';
|
|
import { useUser } from '../../contexts/UserContext';
|
|
import { format, parseISO } from 'date-fns';
|
|
import Modal from '../../components/common/Modal';
|
|
|
|
const LessonPage: React.FC = () => {
|
|
const { lessonId } = useParams<{ lessonId: string }>();
|
|
const navigate = useNavigate();
|
|
const { profile } = useUser();
|
|
const {
|
|
currentLesson,
|
|
currentTimetable,
|
|
currentClass,
|
|
lessonDetailLoading,
|
|
lessonDetailError,
|
|
fetchLessonDetail,
|
|
deleteLesson,
|
|
updateAttendance,
|
|
clearCurrentLesson,
|
|
} = useTimetableStore();
|
|
|
|
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
|
|
const [attendanceUpdating, setAttendanceUpdating] = useState<string | null>(null);
|
|
|
|
useEffect(() => {
|
|
if (lessonId) {
|
|
fetchLessonDetail(lessonId);
|
|
}
|
|
return () => {
|
|
clearCurrentLesson();
|
|
};
|
|
}, [lessonId, fetchLessonDetail, clearCurrentLesson]);
|
|
|
|
const handleDeleteLesson = async () => {
|
|
if (!lessonId) return;
|
|
if (confirm('Are you sure you want to delete this lesson?')) {
|
|
await deleteLesson(lessonId);
|
|
navigate(`/timetable/timetables/${currentTimetable?.id}`);
|
|
}
|
|
};
|
|
|
|
const handleAttendanceUpdate = async (studentId: string, status: 'present' | 'absent' | 'late' | 'excused') => {
|
|
if (!lessonId) return;
|
|
setAttendanceUpdating(studentId);
|
|
await updateAttendance(lessonId, studentId, status);
|
|
setAttendanceUpdating(null);
|
|
};
|
|
|
|
const getStatusColor = (status: string) => {
|
|
switch (status) {
|
|
case 'present': return 'bg-green-100 text-green-700';
|
|
case 'absent': return 'bg-red-100 text-red-700';
|
|
case 'late': return 'bg-yellow-100 text-yellow-700';
|
|
case 'excused': return 'bg-blue-100 text-blue-700';
|
|
default: return 'bg-gray-100 text-gray-700';
|
|
}
|
|
};
|
|
|
|
const isTeacher = profile?.role === 'teacher' || profile?.role === 'admin';
|
|
const isOwner = currentClass?.teacher_id === profile?.id;
|
|
const canManage = isTeacher && isOwner;
|
|
|
|
if (lessonDetailLoading) {
|
|
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 (lessonDetailError) {
|
|
return (
|
|
<div className="bg-red-50 border border-red-200 rounded-lg p-4">
|
|
<p className="text-red-700">{lessonDetailError}</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (!currentLesson) {
|
|
return (
|
|
<div className="text-center py-12">
|
|
<p className="text-gray-500">Lesson not found</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="max-w-4xl mx-auto">
|
|
{/* Breadcrumb */}
|
|
<nav className="mb-4">
|
|
<ol className="flex items-center space-x-2 text-sm text-gray-500">
|
|
<li>
|
|
<Link to="/timetable" className="hover:text-blue-600">Timetable</Link>
|
|
</li>
|
|
<li>/</li>
|
|
<li>
|
|
<Link to={`/timetable/classes/${currentClass?.id}`} className="hover:text-blue-600">
|
|
{currentClass?.name}
|
|
</Link>
|
|
</li>
|
|
<li>/</li>
|
|
<li>
|
|
<Link to={`/timetable/timetables/${currentTimetable?.id}`} className="hover:text-blue-600">
|
|
{currentTimetable?.title}
|
|
</Link>
|
|
</li>
|
|
<li>/</li>
|
|
<li className="text-gray-900 font-medium">Lesson</li>
|
|
</ol>
|
|
</nav>
|
|
|
|
{/* Header */}
|
|
<div className="flex items-start justify-between mb-6">
|
|
<div>
|
|
<Link
|
|
to={`/timetable/timetables/${currentTimetable?.id}`}
|
|
className="inline-flex items-center text-gray-500 hover:text-gray-700 mb-2"
|
|
>
|
|
<ArrowBack size={16} className="mr-1" />
|
|
Back to Timetable
|
|
</Link>
|
|
<h1 className="text-3xl font-bold text-gray-900">{currentLesson.title}</h1>
|
|
{currentLesson.description && (
|
|
<p className="text-gray-600 mt-1">{currentLesson.description}</p>
|
|
)}
|
|
</div>
|
|
{canManage && (
|
|
<div className="flex gap-2">
|
|
<button
|
|
onClick={() => setIsEditModalOpen(true)}
|
|
className="inline-flex items-center px-3 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors"
|
|
>
|
|
<Edit size={16} className="mr-2" />
|
|
Edit
|
|
</button>
|
|
<button
|
|
onClick={handleDeleteLesson}
|
|
className="inline-flex items-center px-3 py-2 bg-red-600 text-white rounded-md hover:bg-red-700 transition-colors"
|
|
>
|
|
<Delete size={16} className="mr-2" />
|
|
Delete
|
|
</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Lesson Details */}
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8">
|
|
<div className="bg-white rounded-lg shadow p-6">
|
|
<h2 className="text-lg font-semibold text-gray-900 mb-4 flex items-center">
|
|
<AccessTime size={20} className="mr-2 text-blue-600" />
|
|
Time & Location
|
|
</h2>
|
|
<div className="space-y-3">
|
|
<div className="flex items-start gap-3">
|
|
<CalendarToday size={18} className="text-gray-400 mt-0.5" />
|
|
<div>
|
|
<p className="font-medium text-gray-900">Date</p>
|
|
<p className="text-gray-600">
|
|
{format(parseISO(currentLesson.start_time), 'EEEE, MMMM d, yyyy')}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<div className="flex items-start gap-3">
|
|
<AccessTime size={18} className="text-gray-400 mt-0.5" />
|
|
<div>
|
|
<p className="font-medium text-gray-900">Time</p>
|
|
<p className="text-gray-600">
|
|
{format(parseISO(currentLesson.start_time), 'HH:mm')} - {format(parseISO(currentLesson.end_time), 'HH:mm')}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
{(currentLesson.location || currentLesson.room) && (
|
|
<div className="flex items-start gap-3">
|
|
<LocationOn size={18} className="text-gray-400 mt-0.5" />
|
|
<div>
|
|
<p className="font-medium text-gray-900">Location</p>
|
|
<p className="text-gray-600">
|
|
{currentLesson.location}
|
|
{currentLesson.location && currentLesson.room && ' - '}
|
|
{currentLesson.room && `Room ${currentLesson.room}`}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="bg-white rounded-lg shadow p-6">
|
|
<h2 className="text-lg font-semibold text-gray-900 mb-4 flex items-center">
|
|
<MenuBook size={20} className="mr-2 text-green-600" />
|
|
Class Information
|
|
</h2>
|
|
<div className="space-y-3">
|
|
<div>
|
|
<p className="text-sm text-gray-500">Class</p>
|
|
<p className="font-medium text-gray-900">{currentClass?.name}</p>
|
|
</div>
|
|
<div>
|
|
<p className="text-sm text-gray-500">Timetable</p>
|
|
<p className="font-medium text-gray-900">{currentTimetable?.title}</p>
|
|
</div>
|
|
{currentLesson.subject && (
|
|
<div>
|
|
<p className="text-sm text-gray-500">Subject</p>
|
|
<p className="font-medium text-gray-900">{currentLesson.subject}</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Attendance Section */}
|
|
{currentLesson.attendance && currentLesson.attendance.length > 0 && (
|
|
<div className="bg-white rounded-lg shadow">
|
|
<div className="px-6 py-4 border-b border-gray-200">
|
|
<h2 className="text-lg font-semibold text-gray-900 flex items-center">
|
|
<People size={20} className="mr-2 text-purple-600" />
|
|
Attendance
|
|
</h2>
|
|
</div>
|
|
<div className="divide-y divide-gray-200">
|
|
{currentLesson.attendance.map((record) => (
|
|
<div key={record.student_id} className="px-6 py-4 flex items-center justify-between">
|
|
<div className="flex items-center gap-3">
|
|
<div className="h-8 w-8 rounded-full bg-blue-100 flex items-center justify-center text-blue-600 font-medium">
|
|
{record.student?.first_name?.[0]}{record.student?.last_name?.[0]}
|
|
</div>
|
|
<div>
|
|
<p className="font-medium text-gray-900">
|
|
{record.student?.first_name} {record.student?.last_name}
|
|
</p>
|
|
<p className="text-sm text-gray-500">{record.student?.email}</p>
|
|
</div>
|
|
</div>
|
|
{canManage ? (
|
|
<div className="flex items-center gap-2">
|
|
<select
|
|
value={record.status}
|
|
onChange={(e) => handleAttendanceUpdate(record.student_id, e.target.value as any)}
|
|
disabled={attendanceUpdating === record.student_id}
|
|
className="text-sm border border-gray-300 rounded-md px-2 py-1 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
|
>
|
|
<option value="present">Present</option>
|
|
<option value="absent">Absent</option>
|
|
<option value="late">Late</option>
|
|
<option value="excused">Excused</option>
|
|
</select>
|
|
{attendanceUpdating === record.student_id && (
|
|
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-blue-600"></div>
|
|
)}
|
|
</div>
|
|
) : (
|
|
<span className={`px-2 py-1 text-xs font-medium rounded capitalize ${getStatusColor(record.status)}`}>
|
|
{record.status}
|
|
</span>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Edit Modal Placeholder */}
|
|
<Modal
|
|
isOpen={isEditModalOpen}
|
|
onClose={() => setIsEditModalOpen(false)}
|
|
title="Edit Lesson"
|
|
>
|
|
<p className="text-gray-600">Lesson editing form would go here...</p>
|
|
<div className="mt-4 flex justify-end gap-2">
|
|
<button
|
|
onClick={() => setIsEditModalOpen(false)}
|
|
className="px-4 py-2 text-gray-600 hover:text-gray-800"
|
|
>
|
|
Cancel
|
|
</button>
|
|
<button
|
|
onClick={() => setIsEditModalOpen(false)}
|
|
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700"
|
|
>
|
|
Save Changes
|
|
</button>
|
|
</div>
|
|
</Modal>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default LessonPage;
|