import React, { useEffect, useState, useCallback } from 'react'; import { useParams, useNavigate } from 'react-router-dom'; import { Box, Typography, Button, CircularProgress, Alert, Chip, Divider, Dialog, DialogTitle, DialogContent, DialogActions, TextField, Autocomplete, IconButton, Tooltip, Tabs, Tab, Avatar, } from '@mui/material'; import { ArrowBack, PersonAdd, PersonRemove, CheckCircle, Cancel, School, } 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 Profile { id: string; full_name: string; display_name?: string; email: string; } interface ClassTeacher { teacher_id: string; is_primary: boolean; can_edit: boolean; profile: Profile; } interface ClassStudent { student_id: string; status: string; enrolled_at: string; profile: Profile; } interface EnrollmentRequest { id: string; student_id: string; status: string; created_at: string; profile: Profile; } interface ClassDetail { id: string; name: string; class_code?: string; subject?: string; year_group?: string; description?: string; is_active: boolean; teachers: ClassTeacher[]; students: ClassStudent[]; enrollment_requests: EnrollmentRequest[]; student_count: number; } // ─── Helpers ────────────────────────────────────────────────────────────────── function initials(name: string) { return name.split(' ').map(w => w[0]).slice(0, 2).join('').toUpperCase(); } // ─── Add Student Dialog ─────────────────────────────────────────────────────── interface AddStudentDialogProps { open: boolean; onClose: () => void; onAdd: (studentId: string) => Promise; accessToken: string; existingIds: Set; } function AddStudentDialog({ open, onClose, onAdd, accessToken, existingIds }: AddStudentDialogProps) { const [allStudents, setAllStudents] = useState([]); const [selected, setSelected] = useState(null); const [loading, setLoading] = useState(false); const [saving, setSaving] = useState(false); useEffect(() => { if (!open) return; setLoading(true); fetch(`${API_BASE}/classes/school/students`, { headers: { Authorization: `Bearer ${accessToken}` }, }) .then(r => r.json()) .then(d => setAllStudents((d.students || []).filter((s: Profile) => !existingIds.has(s.id)))) .finally(() => setLoading(false)); }, [open, accessToken, existingIds]); const handleAdd = async () => { if (!selected) return; setSaving(true); await onAdd(selected.id); setSaving(false); setSelected(null); onClose(); }; return ( Add Student to Class {loading ? ( ) : ( `${o.full_name} (${o.email})`} value={selected} onChange={(_, v) => setSelected(v)} renderInput={params => ( )} sx={{ mt: 1 }} /> )} ); } // ─── Main page ──────────────────────────────────────────────────────────────── const ClassDetailPage: React.FC = () => { const { classId } = useParams<{ classId: string }>(); const navigate = useNavigate(); const { accessToken, user, bootstrapData } = useAuth(); const [cls, setCls] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [tab, setTab] = useState(0); const [isAdmin, setIsAdmin] = useState(false); const [addOpen, setAddOpen] = useState(false); const [actionError, setActionError] = useState(null); const load = useCallback(async () => { if (!accessToken || !classId) return; setLoading(true); setError(null); try { const clsRes = await fetch(`${API_BASE}/classes/${classId}`, { headers: { Authorization: `Bearer ${accessToken}` }, }).then(r => r.json()); if (clsRes.id) setCls(clsRes); else setError(clsRes.detail || 'Class not found'); const role = bootstrapData?.active_institute?.membership_role || ''; setIsAdmin(role === 'school_admin' || role === 'department_head'); } catch (e: any) { setError(e.message); } finally { setLoading(false); } }, [accessToken, classId, bootstrapData]); useEffect(() => { load(); }, [load]); const apiPost = async (path: string, body?: object) => { const r = await fetch(`${API_BASE}${path}`, { method: 'POST', headers: { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json' }, body: body ? JSON.stringify(body) : undefined, }); return r.json(); }; const apiDelete = async (path: string) => { await fetch(`${API_BASE}${path}`, { method: 'DELETE', headers: { Authorization: `Bearer ${accessToken}` }, }); }; const apiPatch = async (path: string, body: object) => { const r = await fetch(`${API_BASE}${path}`, { method: 'PATCH', headers: { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json' }, body: JSON.stringify(body), }); return r.json(); }; const handleAddStudent = async (studentId: string) => { setActionError(null); const res = await apiPost(`/classes/${classId}/students`, { student_id: studentId }); if (res.status === 'ok') load(); else setActionError(res.detail || 'Failed to add student'); }; const handleRemoveStudent = async (studentId: string) => { setActionError(null); await apiDelete(`/classes/${classId}/students/${studentId}`); load(); }; const handleEnrollmentResponse = async (requestId: string, action: 'approve' | 'reject') => { setActionError(null); const res = await apiPatch(`/classes/${classId}/enrollment-requests/${requestId}`, { action }); if (res.status === 'ok') load(); else setActionError(res.detail || 'Action failed'); }; if (loading) { return ( ); } if (error || !cls) { return ( {error || 'Class not found'} ); } const existingStudentIds = new Set(cls.students.map(s => s.student_id)); const pendingCount = cls.enrollment_requests.length; return ( {/* Header */} {cls.name} {cls.class_code && } {cls.subject && } {cls.year_group && } {cls.description && ( {cls.description} )} {actionError && ( setActionError(null)} sx={{ mb: 2 }}> {actionError} )} {/* Tabs */} setTab(v)} sx={{ borderBottom: 1, borderColor: 'divider', mb: 2 }}> 0 ? ` (${pendingCount})` : ''}`} /> {/* Students tab */} {tab === 0 && ( {isAdmin && ( )} {cls.students.length === 0 ? ( No students enrolled yet ) : ( {cls.students.map(s => ( {initials(s.profile?.full_name || '?')} {s.profile?.full_name || s.student_id} {s.profile?.email} {isAdmin && ( handleRemoveStudent(s.student_id)} > )} ))} )} )} {/* Enrollment requests tab */} {tab === 1 && ( {cls.enrollment_requests.length === 0 ? ( No pending enrollment requests ) : ( {cls.enrollment_requests.map(req => ( {initials(req.profile?.full_name || '?')} {req.profile?.full_name || req.student_id} {req.profile?.email} · requested{' '} {new Date(req.created_at).toLocaleDateString('en-GB')} {isAdmin && ( handleEnrollmentResponse(req.id, 'approve')} > handleEnrollmentResponse(req.id, 'reject')} > )} ))} )} )} {/* Teachers tab */} {tab === 2 && ( {cls.teachers.length === 0 ? ( No teachers assigned ) : ( cls.teachers.map(t => ( {initials(t.profile?.full_name || '?')} {t.profile?.full_name || t.teacher_id} {t.profile?.email} {t.is_primary && ( )} )) )} )} setAddOpen(false)} onAdd={handleAddStudent} accessToken={accessToken || ''} existingIds={existingStudentIds} /> ); }; export default ClassDetailPage;