File size: 6,988 Bytes
f002ae5
 
bc45e54
 
f002ae5
 
bc45e54
f002ae5
9f97e28
bc45e54
 
f002ae5
 
 
 
 
9f97e28
f002ae5
 
9f97e28
 
bc45e54
 
9f97e28
 
 
 
 
 
 
 
 
 
 
 
 
 
f002ae5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
bc45e54
 
 
f002ae5
bc45e54
 
 
 
 
 
f002ae5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
bc45e54
 
 
f002ae5
bc45e54
 
f002ae5
 
 
 
 
 
9f97e28
f002ae5
 
 
9f97e28
 
 
 
 
 
 
 
 
 
 
bc45e54
9f97e28
 
 
 
 
 
 
 
 
bc45e54
f002ae5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
bc45e54
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
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 }>;
}

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<HTMLIFrameElement>(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 {
      // When generation finishes, show the last completed one
      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 activeReport = reports[activeIndex];
  const doneCount = reports.filter((r) => r.status === "done").length;
  const errorCount = reports.filter((r) => r.status === "error").length;

  const 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);
  };

  return (
    <div className="space-y-4">
      {/* Progress bar */}
      {isGenerating && (
        <div className="card p-4">
          <div className="flex items-center justify-between mb-2">
            <span className="text-sm font-semibold text-[var(--color-text)]">
              生成進度
            </span>
            <span className="text-sm text-[var(--color-text-muted)]">
              {doneCount + errorCount} / {reports.length}
            </span>
          </div>
          <div className="w-full h-2 bg-[var(--color-border)] rounded-full overflow-hidden">
            <div
              className="h-full bg-gradient-to-r from-[var(--color-primary)] to-[var(--color-success)] transition-all duration-500"
              style={{
                width: `${((doneCount + errorCount) / reports.length) * 100}%`,
              }}
            />
          </div>
        </div>
      )}

      {/* Action bar */}
      <div className="flex flex-wrap items-center gap-3">
        <button onClick={onBack} className="btn btn-outline text-sm">
          ← 返回選擇學生
        </button>
        <div className="flex-1" />
        {activeReport?.status === "done" && (
          <button onClick={handleDownloadSingle} className="btn btn-outline text-sm">
            下載此報告
          </button>
        )}
        {doneCount > 1 && (
          <button onClick={onExportAllHtml} className="btn btn-outline text-sm">
            下載全部報告
          </button>
        )}
        {onSaveToDb && !isGenerating && doneCount > 0 && (
          <button
            onClick={handleSaveToDb}
            disabled={saveState === "saving" || saveState === "saved"}
            className="btn btn-primary text-sm disabled:opacity-60"
          >
            {saveState === "saving" && "儲存中..."}
            {saveState === "saved" && "✅ 已儲存"}
            {(saveState === "idle" || saveState === "error") && "儲存至資料庫"}
          </button>
        )}
      </div>
      {saveMsg && (
        <p
          className={`text-sm text-right ${
            saveState === "error" ? "text-red-400" : "text-[var(--color-success)]"
          }`}
        >
          {saveMsg}
        </p>
      )}

      {/* Student tabs */}
      <div className="flex gap-2 overflow-x-auto pb-2">
        {reports.map((r, i) => (
          <button
            key={r.studentIndex}
            onClick={() => 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)]"
            }`}
          >
            <span className="mr-1.5">
              {r.status === "done" && "✅"}
              {r.status === "generating" && "⏳"}
              {r.status === "pending" && "⬜"}
              {r.status === "error" && "❌"}
            </span>
            {r.studentName}
          </button>
        ))}
      </div>

      {/* Report content */}
      <div className="card overflow-hidden" style={{ height: "70vh" }}>
        {activeReport?.status === "done" && (
          <iframe
            ref={iframeRef}
            srcDoc={activeReport.html}
            title={`Report - ${activeReport.studentName}`}
            className="w-full h-full border-0"
            sandbox="allow-scripts allow-same-origin"
          />
        )}

        {activeReport?.status === "generating" && (
          <div className="w-full h-full flex flex-col items-center justify-center gap-4">
            <div className="w-10 h-10 border-3 border-[var(--color-primary)] border-t-transparent rounded-full animate-spin" />
            <p className="text-[var(--color-text-muted)] text-lg">
              正在為 <strong className="text-[var(--color-text)]">{activeReport.studentName}</strong> 生成報告...
            </p>
          </div>
        )}

        {activeReport?.status === "pending" && (
          <div className="w-full h-full flex items-center justify-center">
            <p className="text-[var(--color-text-muted)]">
              等待生成...
            </p>
          </div>
        )}

        {activeReport?.status === "error" && (
          <div className="w-full h-full flex flex-col items-center justify-center gap-3">
            <span className="text-4xl"></span>
            <p className="text-[var(--color-text)] font-semibold">
              {activeReport.studentName} 報告生成失敗
            </p>
            <p className="text-sm text-[var(--color-text-muted)] max-w-md text-center">
              {activeReport.error}
            </p>
          </div>
        )}
      </div>
    </div>
  );
}