import { useEffect, useState } from "react"; import { apiGet, apiPut, apiDelete } from "../../lib/api"; type Tab = "overview" | "teachers" | "sessions" | "students"; interface Stats { teachers: number; students: number; quizzes: number; reports: number; student_results: number; prompts: number; schema_version: string | null; } interface TeacherRow { id: number; email: string; full_name: string; school: string; is_admin: boolean; created_at: string; quiz_count: number; student_count: number; } interface SessionRow { id: number; title: string; subject: string | null; status: string; teacher_email: string; report_count: number; created_at: string; } interface StudentRow { id: number; name: string; teacher_email: string; result_count: number; avg_score: number | null; } interface TimelineEntry { quiz_title: string; quiz_subject: string | null; quiz_created_at: string; total_questions: number; correct_count: number; score: number; } const TABS: { key: Tab; label: string }[] = [ { key: "overview", label: "總覽" }, { key: "teachers", label: "教師" }, { key: "sessions", label: "測驗" }, { key: "students", label: "學生" }, ]; export function AdminDashboard() { const [tab, setTab] = useState("overview"); return (

管理後台

資料庫管理與使用分析

{TABS.map((t) => ( ))}
{tab === "overview" && } {tab === "teachers" && } {tab === "sessions" && } {tab === "students" && }
); } function useFetch(path: string, deps: unknown[] = []) { const [data, setData] = useState(null); const [error, setError] = useState(""); const [loading, setLoading] = useState(true); const [nonce, setNonce] = useState(0); const reload = () => setNonce((n) => n + 1); useEffect(() => { let active = true; setLoading(true); apiGet(path) .then((d) => active && setData(d)) .catch((e) => active && setError(e instanceof Error ? e.message : "Error")) .finally(() => active && setLoading(false)); return () => { active = false; }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [path, nonce, ...deps]); return { data, error, loading, reload }; } function StatCard({ label, value }: { label: string; value: number | string }) { return (
{value}
{label}
); } function Overview() { const { data, loading, error } = useFetch("/api/admin/stats"); if (loading) return ; if (error || !data) return ; return (

資料庫結構版本:{data.schema_version ?? "未知"}

); } function Teachers() { const { data, loading, error, reload } = useFetch<{ users: TeacherRow[] }>("/api/admin/users"); if (loading) return ; if (error || !data) return ; const toggleAdmin = async (u: TeacherRow) => { await apiPut(`/api/admin/users/${u.id}/admin`, { is_admin: !u.is_admin }); reload(); }; const remove = async (u: TeacherRow) => { if (!confirm(`刪除教師 ${u.email}?此操作會一併刪除其所有測驗與報告。`)) return; await apiDelete(`/api/admin/users/${u.id}`); reload(); }; return ( {data.users.map((u) => ( ))}
{u.email} {u.full_name || "—"} {u.school || "—"} {u.quiz_count} {u.student_count}
); } function Sessions() { const { data, loading, error, reload } = useFetch<{ sessions: SessionRow[] }>("/api/admin/sessions"); if (loading) return ; if (error || !data) return ; const remove = async (s: SessionRow) => { if (!confirm(`刪除測驗「${s.title}」?`)) return; await apiDelete(`/api/admin/sessions/${s.id}`); reload(); }; return ( {data.sessions.map((s) => ( ))}
{s.title} {s.subject || "—"} {s.teacher_email} {s.status} {s.report_count} {new Date(s.created_at).toLocaleDateString()}
); } function Students() { const { data, loading, error } = useFetch<{ students: StudentRow[] }>("/api/admin/students"); const [selected, setSelected] = useState(null); if (loading) return ; if (error || !data) return ; if (selected !== null) { return setSelected(null)} />; } return ( {data.students.map((s) => ( ))}
{s.name} {s.teacher_email} {s.result_count} {s.avg_score ?? "—"}
); } function StudentDetail({ studentId, onBack }: { studentId: number; onBack: () => void }) { const { data, loading, error } = useFetch<{ student: { name: string }; timeline: TimelineEntry[]; }>(`/api/admin/students/${studentId}`); if (loading) return ; if (error || !data) return ; return (

{data.student.name} 的跨測驗歷程

{data.timeline.length === 0 ? (

尚無成績紀錄

) : ( {data.timeline.map((t, i) => ( ))}
{t.quiz_title} {t.quiz_subject || "—"} {new Date(t.quiz_created_at).toLocaleDateString()} {t.correct_count} {t.total_questions} {t.score}
)}
); } function Table({ headers, children }: { headers: string[]; children: React.ReactNode }) { return (
{headers.map((h, i) => ( ))} {children}
{h}
); } function Td({ children }: { children: React.ReactNode }) { return {children}; } function Spinner() { return (
); } function ErrorBox({ msg }: { msg: string }) { return
{msg || "載入失敗"}
; }