app/src/pages/timetable/LessonPage.tsx
Agent Zero 067df34c50 fix: correct Material UI icon naming in Header.tsx and update timetable components
- 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
2026-02-26 07:28:47 +00:00

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;