import { useState, useRef, useEffect } from "react"; import type { StudentReport } from "../../App"; interface ReportViewerProps { reports: StudentReport[]; isGenerating: boolean; onBack: () => void; onExportAllHtml: () => void; onSaveToDb?: () => Promise<{ students_saved: number }>; } function CoffeeCup({ size = 32 }: { size?: number }) { const w = size; const h = Math.round(size * (380 / 300)); return ( ); } function estimateMinutes(total: number): string { const secs = total * 45; if (secs < 90) return "about a minute"; const mins = Math.ceil(secs / 60); return `about ${mins} minutes`; } export function ReportViewer({ reports, isGenerating, onBack, onExportAllHtml, onSaveToDb, }: ReportViewerProps) { const [activeIndex, setActiveIndex] = useState(0); const [saveState, setSaveState] = useState<"idle" | "saving" | "saved" | "error">("idle"); const [saveMsg, setSaveMsg] = useState(""); const iframeRef = useRef(null); const topRef = useRef(null); const handleSaveToDb = async () => { if (!onSaveToDb) return; setSaveState("saving"); setSaveMsg(""); try { const res = await onSaveToDb(); setSaveState("saved"); setSaveMsg(`已儲存 ${res.students_saved} 位學生成績`); } catch (e) { setSaveState("error"); setSaveMsg(e instanceof Error ? e.message : "儲存失敗"); } }; // Auto-switch to the report currently being generated useEffect(() => { const generatingIdx = reports.findIndex((r) => r.status === "generating"); if (generatingIdx >= 0) { setActiveIndex(generatingIdx); } else { let lastDone = -1; for (let j = reports.length - 1; j >= 0; j--) { if (reports[j].status === "done") { lastDone = j; break; } } if (lastDone >= 0) setActiveIndex(lastDone); } }, [reports]); const scrollToTop = () => { iframeRef.current?.contentWindow?.scrollTo({ top: 0, behavior: "smooth" }); }; const activeReport = reports[activeIndex]; const doneCount = reports.filter((r) => r.status === "done").length; const errorCount = reports.filter((r) => r.status === "error").length; const totalCount = reports.length; const handleDownloadAll = () => { const missing = totalCount - doneCount; if (missing > 0) { const ok = window.confirm( `尚有 ${missing} 位學生的報告尚未生成完成,這份下載檔將不完整(只包含目前已完成的 ${doneCount} 份報告,缺少的學生不會出現在檔案中)。\n\n確定要現在下載嗎?` ); if (!ok) return; } onExportAllHtml(); }; return ( {/* Progress bar */} {isGenerating && ( 生成進度 {doneCount + errorCount} / {totalCount} Hang tight — this will take {estimateMinutes(totalCount)}. Grab a coffee! )} {/* Action bar */} ← 返回選擇學生 {activeReport?.status === "done" && ( 下載此報告 )} {doneCount > 1 && ( 下載全部報告 )} {onSaveToDb && !isGenerating && doneCount > 0 && ( {saveState === "saving" && "儲存中..."} {saveState === "saved" && "已儲存"} {(saveState === "idle" || saveState === "error") && "儲存至資料庫"} )} {saveMsg && ( {saveMsg} )} {/* Student tabs */} {reports.map((r, i) => ( setActiveIndex(i)} className={`shrink-0 px-4 py-2 rounded-xl text-sm font-semibold transition-all ${ i === activeIndex ? "bg-[var(--color-primary)] text-white shadow-md" : "bg-[var(--color-surface)] text-[var(--color-text-muted)] hover:text-[var(--color-text)] border border-[var(--color-border)]" }`} > {r.studentName} ))} {/* Report content */} {activeReport?.status === "done" && ( <> {/* Floating back-to-top button */} > )} {activeReport?.status === "generating" && ( 正在為 {activeReport.studentName} 生成報告... )} {activeReport?.status === "pending" && ( 等待生成... )} {activeReport?.status === "error" && ( ✕ {activeReport.studentName} 報告生成失敗 {activeReport.error} )} ); function handleDownloadSingle() { if (!activeReport?.html) return; const blob = new Blob([activeReport.html], { type: "text/html" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = `classlens-${activeReport.studentName}.html`; a.click(); URL.revokeObjectURL(url); } }
Hang tight — this will take {estimateMinutes(totalCount)}. Grab a coffee!
{saveMsg}
正在為 {activeReport.studentName} 生成報告...
等待生成...
{activeReport.studentName} 報告生成失敗
{activeReport.error}