import React, { useEffect, useState, useCallback } from 'react'; import { useNavigate } from 'react-router-dom'; import { Box, Typography, Button, CircularProgress, Alert, Chip, Dialog, DialogTitle, DialogContent, DialogActions, TextField, Select, MenuItem, FormControl, InputLabel, Divider, IconButton, Tooltip, List, ListItemButton, ListItemText, InputAdornment, } from '@mui/material'; import { ChevronLeft, ChevronRight, Today, EditNote, LibraryAdd, Search, OpenInNew, } from '@mui/icons-material'; import { useAuth } from '../../contexts/AuthContext'; const API_BASE = import.meta.env.VITE_API_BASE || import.meta.env.VITE_API_URL || '/api'; // ─── Types ──────────────────────────────────────────────────────────────────── interface Lesson { id: string; date: string; period_code: string; period_name: string; start_time: string | null; end_time: string | null; class_name: string | null; subject: string | null; year_group: string | null; week_cycle: string; day_of_week: string; status: string; lesson_plan: Record; notes: string | null; whiteboard_room_id: string | null; } interface DayEntry { date: string; day_of_week: string; is_today: boolean; lessons: Lesson[]; } // ─── Helpers ────────────────────────────────────────────────────────────────── function toMonday(d: Date): string { const day = d.getDay(); const diff = day === 0 ? -6 : 1 - day; const monday = new Date(d); monday.setDate(d.getDate() + diff); return monday.toISOString().slice(0, 10); } function addWeeks(isoDate: string, n: number): string { const d = new Date(isoDate + 'T00:00:00'); d.setDate(d.getDate() + n * 7); return d.toISOString().slice(0, 10); } function formatDate(isoDate: string): string { return new Date(isoDate + 'T00:00:00').toLocaleDateString('en-GB', { day: 'numeric', month: 'short', }); } const STATUS_COLORS: Record = { planned: 'default', in_progress: 'primary', completed: 'success', cancelled: 'error', substituted: 'warning', }; // ─── Lesson edit dialog ─────────────────────────────────────────────────────── interface EditDialogProps { lesson: Lesson | null; onClose: () => void; onSave: (id: string, updates: { notes?: string; status?: string }) => Promise; } function LessonEditDialog({ lesson, onClose, onSave }: EditDialogProps) { const [notes, setNotes] = useState(''); const [status, setStatus] = useState('planned'); const [saving, setSaving] = useState(false); useEffect(() => { if (lesson) { setNotes(lesson.notes || ''); setStatus(lesson.status || 'planned'); } }, [lesson]); const handleSave = async () => { if (!lesson) return; setSaving(true); await onSave(lesson.id, { notes, status }); setSaving(false); onClose(); }; if (!lesson) return null; return ( {lesson.class_name || lesson.period_code} {lesson.date} · {lesson.period_name} {lesson.start_time && ` · ${lesson.start_time}–${lesson.end_time}`} {lesson.week_cycle && ` · Week ${lesson.week_cycle}`} Status setNotes(e.target.value)} multiline minRows={3} fullWidth size="small" placeholder="Lesson notes, objectives, reminders…" /> ); } // ─── Lesson card ────────────────────────────────────────────────────────────── interface LessonCardProps { lesson: Lesson; onEdit: (l: Lesson) => void; onAssignPlan: (l: Lesson) => void; } function LessonCard({ lesson, onEdit, onAssignPlan }: LessonCardProps) { return ( {lesson.class_name || lesson.period_code} onAssignPlan(lesson)} sx={{ p: 0.25 }}> onEdit(lesson)} sx={{ p: 0.25 }}> {lesson.subject && ( {lesson.subject}{lesson.year_group ? ` · Y${lesson.year_group}` : ''} )} {lesson.start_time ? `${lesson.start_time}–${lesson.end_time}` : lesson.period_name} {lesson.week_cycle && ( )} {lesson.notes && ( {lesson.notes.length > 80 ? lesson.notes.slice(0, 80) + '…' : lesson.notes} )} ); } // ─── Main page ──────────────────────────────────────────────────────────────── const TaughtLessonsPage: React.FC = () => { const { accessToken } = useAuth(); const navigate = useNavigate(); const [weekStart, setWeekStart] = useState(toMonday(new Date())); const [days, setDays] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [materializing, setMaterializing] = useState(false); const [materializeResult, setMaterializeResult] = useState(null); const [editingLesson, setEditingLesson] = useState(null); const [assigningLesson, setAssigningLesson] = useState(null); const [plans, setPlans] = useState<{ id: string; title: string; subject: string | null }[]>([]); const [planSearch, setPlanSearch] = useState(''); const [assignSaving, setAssignSaving] = useState(false); const [assignResult, setAssignResult] = useState(null); const loadLessons = useCallback(async (ws: string) => { if (!accessToken) return; setLoading(true); setError(null); try { const res = await fetch( `${API_BASE}/timetable/lessons?week_start=${ws}&weeks=1`, { headers: { Authorization: `Bearer ${accessToken}` } } ); const data = await res.json(); if (data.status === 'ok') { setDays(data.days); } else { setError(data.message || 'Failed to load lessons'); } } catch (e: any) { setError(e.message); } finally { setLoading(false); } }, [accessToken]); useEffect(() => { loadLessons(weekStart); }, [weekStart, loadLessons]); const goThisWeek = () => setWeekStart(toMonday(new Date())); const prevWeek = () => setWeekStart(ws => addWeeks(ws, -1)); const nextWeek = () => setWeekStart(ws => addWeeks(ws, 1)); const handleMaterialize = async () => { if (!accessToken) return; setMaterializing(true); setMaterializeResult(null); try { const res = await fetch(`${API_BASE}/timetable/materialize`, { method: 'POST', headers: { Authorization: `Bearer ${accessToken}` }, }); const data = await res.json(); if (data.status === 'ok') { setMaterializeResult(`Created ${data.lessons_upserted} lessons, ${data.whiteboard_rooms_created} rooms`); loadLessons(weekStart); } else { setMaterializeResult(`Error: ${data.message}`); } } catch (e: any) { setMaterializeResult(`Error: ${e.message}`); } finally { setMaterializing(false); } }; const handleSaveLesson = async (id: string, updates: { notes?: string; status?: string }) => { if (!accessToken) return; await fetch(`${API_BASE}/timetable/lessons/${id}`, { method: 'PATCH', headers: { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json' }, body: JSON.stringify(updates), }); loadLessons(weekStart); }; const openAssignDialog = async (lesson: Lesson) => { setAssigningLesson(lesson); setPlanSearch(''); setAssignResult(null); if (plans.length === 0) { try { const res = await fetch(`${API_BASE}/lessons/plans`, { headers: { Authorization: `Bearer ${accessToken}` }, }); const data = await res.json(); if (Array.isArray(data)) setPlans(data); } catch { /* silently ignore */ } } }; const handleAssignPlan = async (planId: string) => { if (!assigningLesson) return; setAssignSaving(true); try { const res = await fetch(`${API_BASE}/lessons/plans/${planId}/deliver`, { method: 'POST', headers: { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ taught_lesson_id: assigningLesson.id }), }); const data = await res.json(); if (data.id) { setAssignResult('Plan linked ✓'); setTimeout(() => { setAssigningLesson(null); setAssignResult(null); }, 1200); } else { setAssignResult(data.detail || 'Failed to link plan'); } } catch (e: any) { setAssignResult(e.message); } finally { setAssignSaving(false); } }; const filteredPlans = plans.filter(p => !planSearch || p.title.toLowerCase().includes(planSearch.toLowerCase()) || (p.subject || '').toLowerCase().includes(planSearch.toLowerCase()) ); const totalLessons = days.reduce((sum, d) => sum + d.lessons.length, 0); const weekLabel = days.length > 0 ? `${formatDate(days[0].date)} – ${formatDate(days[days.length - 1].date)}` : ''; return ( {/* Header */} My Lessons {weekLabel} · {totalLessons} lesson{totalLessons !== 1 ? 's' : ''} {materializeResult && ( setMaterializeResult(null)} sx={{ mb: 2 }} > {materializeResult} )} {error && {error}} {/* Week grid */} {loading ? ( ) : ( {days.map(day => ( {/* Day header */} {day.day_of_week.slice(0, 3)} {formatDate(day.date)} {/* Lesson cards */} {day.lessons.length === 0 ? ( No lessons ) : ( day.lessons.map(lesson => ( )) )} ))} )} setEditingLesson(null)} onSave={handleSaveLesson} /> {/* Assign Plan dialog */} setAssigningLesson(null)} maxWidth="sm" fullWidth> Assign Lesson Plan {assigningLesson && ( {assigningLesson.class_name || assigningLesson.period_code} · {assigningLesson.date} )} {assignResult ? ( {assignResult} ) : null} setPlanSearch(e.target.value)} InputProps={{ startAdornment: , }} sx={{ mb: 1 }} /> {plans.length === 0 ? ( No lesson plans yet.{' '} ) : ( {filteredPlans.map(p => ( handleAssignPlan(p.id)} disabled={assignSaving} sx={{ borderRadius: 1, mb: 0.25 }} > {assignSaving && } ))} )} ); }; export default TaughtLessonsPage;