top of page

導師的數位分身!「任務指派」與「表現追蹤」自製雙神器,開啟班級管理新紀元

  • 1天前
  • 讀畢需時 33 分鐘

一、 前言:導師的三頭六臂,從數位轉型開始

在繁忙的國小教學現場,導師不僅要準備教學內容,更要管理班級的大小庶務與追蹤數十位學生的學習狀態。過去,我們習慣用密密麻麻的紙本登記簿、便利貼或口頭交辦,但資訊往往破碎、難以即時彙整,且容易造成遺漏。

為了徹底解決這些痛點,我結合 AI 技術(Gemini) 開發了兩款互補的班級經營 APP:一款負責「事」的指派與掌握(The Delegator),一款負責「人」的表現與追蹤(The Tracker)。這套「雙核心系統」將瑣事自動化,讓班級經營變得透明且高效。


二、 系統深蹲:兩款 APP 核心功能全解析

1. 【The Delegator 班級小幫手】—— 班級任務的智慧管家

這款 APP 的設計核心是「高效、公平與透明」,專門處理班級的例行公事與臨時任務。

  • 老師儀表板: 老師的中央指揮中心,即時顯示各幹部(如班長、圖書股長)的進度條。透過顏色標記(已完成、未完成),瞬間掌握全班做事進度。


  • 學生打卡區: 學生端介面簡潔,清楚列出個人負責任務(如:整理圖書櫃),完成後點擊勾選即可即時回報。

  • 常態任務設定: 老師能彈性建立每日或每週的例行任務,並分派給指定學生,系統會自動重置進度。

  • 命運轉盤: 針對臨時公事(如:搬器材),內建篩選功能(自動排除請假或已有職務者),杜絕「每次都找我」的爭議。



2. 【The Tracker 班級主控台】—— 表現與作業的全方位追蹤

如果 Delegator 是處理「事務」,那麼 The Tracker 就是老師的「眼與耳」,負責記錄學生在各科目的課堂表現與作業品質。

  • 導師主控台: 採用卡片式設計顯示學生頭像與總點數,右側設有「未交總覽快訊」與「最新動態」,誰表現好、誰缺作業,一目了然。

  • 全班狀況統整表: 將數據表格化,完整呈現總分、缺交清單(如:生字乙本)及行為紀錄,是親師溝通與期末評語的最佳依據。

  • 靈活系統設定: 老師可自定義學生名單、新增教學情境(如:國語課、數學課、早自修),確保紀錄與教學進度百分之百契合。

  • 模擬科任老師視角: 貼心的行動化介面,支援「批次給分」,讓科任老師或班級幹部能快速紀錄,課堂互動不中斷。

三、 如何將這套工具帶進「你的」教室?

這就是 AI 時代最迷人的地方:你不必懂程式碼,也能成為開發者! 請按照以下步驟,將這兩款 APP 變為你的專屬版本:

第一步:獲取原始碼

點擊我分享的 Gemini 對話連結(The Delegator 連結 / The Tracker 連結),找到包含程式碼的區塊,點擊右上角的「複製」。

第二步:利用 Gemini 進行「無痛修改」

開啟你的 Gemini 介面,貼上程式碼並輸入你的需求,讓 AI 當你的工程師:

範例指令:「這是我同事分享的班級經營 APP 代碼,請幫我把名單改為:王小明、林小華...,並將科目名稱改為:國語、數學、英文。」

第三步:存檔為網頁格式 (關鍵!)

  1. 在電腦桌面按右鍵,新增一個「文字文件 (.txt)」。

  2. 打開文件,貼上 Gemini 修改後的代碼。

  3. 點擊「另存新檔」,將檔名改為 班級主控台.html副檔名一定要是 .html)。

  4. 編碼選擇 「UTF-8」 後存檔。

第四步:設置桌面捷徑

直接雙擊桌面上的這個圖示,APP 就會在瀏覽器中運行。你可以把它存在隨身碟或 Google Drive,在任何教室電腦打開就能立刻開始一天的管理!

四、 結語:讓科技賦能,把時間還給教育

開發這兩款 APP 的初衷,是為了將老師從瑣碎的「行政登記」中解放出來。當數據追蹤自動化,我們就有更多的心力去觀察學生的情緒、引導他們的行為,並專注於教學內容的深度。

科技不應是負擔,而應是老師最強大的後盾。 歡迎大家嘗試自製最適合你班級的管理神器,讓我們在數位教學的路上一起進化!


第一個程式碼:
import React, { useState, useEffect, useRef } from 'react';
import { CheckCircle2, Circle, AlertCircle, ChevronDown, ChevronUp, Settings2, Users, Check, RotateCcw, X, Volume2, Trash2, Plus, UserPlus, ClipboardList, LayoutGrid, List } from 'lucide-react';

// --- Mock Data 假資料 ---
const initialStudents = [
  { id: 1, name: '王小明', avatar: '👦🏻', role: '班長', absent: false },
  { id: 2, name: '林小華', avatar: '👧🏻', role: '圖書股長', absent: false },
  { id: 3, name: '陳阿強', avatar: '👦🏽', role: '風紀股長', absent: false },
  { id: 4, name: '李美美', avatar: '👧🏼', role: '衛生股長', absent: false },
  { id: 5, name: '張胖虎', avatar: '👦🏻', role: '器材股長', absent: false },
  { id: 6, name: '黃小夫', avatar: '👦🏼', role: '無', absent: false },
  { id: 7, name: '吳靜香', avatar: '👧🏻', role: '英文小老師', absent: false },
  { id: 8, name: '劉大雄', avatar: '👦🏻', role: '無', absent: false },
  { id: 9, name: '蔡杉木', avatar: '👦🏻', role: '數學小老師', absent: false },
  { id: 10, name: '周小貓', avatar: '👧🏽', role: '無', absent: true }, // 請假
];

const initialTasks = [
  { id: 101, studentId: 1, title: '帶領全班喊起立敬禮', freq: '每日', completed: true },
  { id: 102, studentId: 2, title: '整理後方圖書櫃', freq: '每週', completed: false },
  { id: 103, studentId: 3, title: '登記午休講話名單', freq: '每日', completed: true },
  { id: 104, studentId: 4, title: '檢查垃圾桶是否需清理', freq: '每日', completed: false },
  { id: 105, studentId: 5, title: '去體育室借體育器材', freq: '每日', completed: false },
  { id: 106, studentId: 7, title: '發放單字小考考卷', freq: '每日', completed: false },
  { id: 107, studentId: 9, title: '收齊數學作業本', freq: '每日', completed: true },
];

const colors = ['#FF9AA2', '#FFB7B2', '#FFDAC1', '#E2F0CB', '#B5EAD7', '#C7CEEA', '#FCE2C6', '#D4F0F0', '#E8DFF5', '#F1CBFF'];

export default function App() {
  const [view, setView] = useState('teacher'); // 'teacher', 'student', 'wheel', 'manage-students', 'manage-tasks'
  const [students, setStudents] = useState(initialStudents);
  const [tasks, setTasks] = useState(initialTasks);

  // 播放成功音效 (微互動)
  const playSuccessSound = () => {
    try {
      const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
      const oscillator = audioCtx.createOscillator();
      const gainNode = audioCtx.createGain();
      oscillator.type = 'sine';
      oscillator.frequency.setValueAtTime(523.25, audioCtx.currentTime); // C5
      oscillator.frequency.exponentialRampToValueAtTime(880, audioCtx.currentTime + 0.1); // A5
      gainNode.gain.setValueAtTime(0.5, audioCtx.currentTime);
      gainNode.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.3);
      oscillator.connect(gainNode);
      gainNode.connect(audioCtx.destination);
      oscillator.start();
      oscillator.stop(audioCtx.currentTime + 0.3);
    } catch (e) {
      console.log('Audio not supported', e);
    }
  };

  const toggleTask = (taskId) => {
    setTasks(tasks.map(t => {
      if (t.id === taskId) {
        if (!t.completed) playSuccessSound(); // 只有完成時播放音效
        return { ...t, completed: !t.completed };
      }
      return t;
    }));
  };

  return (
    <div className="min-h-screen bg-slate-50 font-sans text-slate-800">
      {/* 頂部導航列 */}
      <header className="bg-white shadow-sm px-6 py-4 flex flex-col xl:flex-row items-center justify-between sticky top-0 z-10">
        <div className="flex items-center gap-3 mb-4 xl:mb-0">
          <div className="bg-blue-500 text-white p-2 rounded-xl">
            <Users size={28} />
          </div>
          <h1 className="text-2xl font-bold text-slate-800 tracking-wide whitespace-nowrap">
            The Delegator <span className="text-lg text-slate-500 font-normal">班級小幫手</span>
          </h1>
        </div>
        <div className="flex flex-wrap justify-center gap-2 bg-slate-100 p-1 rounded-2xl">
          <button
            onClick={() => setView('teacher')}
            className={`px-4 py-2 rounded-xl font-bold transition-all text-sm sm:text-base ${view === 'teacher' ? 'bg-white shadow text-blue-600' : 'text-slate-500 hover:bg-slate-200'}`}
          >
            老師儀表板
          </button>
          <button
            onClick={() => setView('student')}
            className={`px-4 py-2 rounded-xl font-bold transition-all text-sm sm:text-base ${view === 'student' ? 'bg-white shadow text-green-600' : 'text-slate-500 hover:bg-slate-200'}`}
          >
            學生打卡區
          </button>
          <button
            onClick={() => setView('wheel')}
            className={`px-4 py-2 rounded-xl font-bold transition-all text-sm sm:text-base ${view === 'wheel' ? 'bg-white shadow text-purple-600' : 'text-slate-500 hover:bg-slate-200'}`}
          >
            命運轉盤
          </button>
          <div className="w-px bg-slate-300 mx-1 hidden sm:block"></div>
          <button
            onClick={() => setView('manage-students')}
            className={`px-4 py-2 rounded-xl font-bold transition-all text-sm sm:text-base flex items-center gap-1 ${view === 'manage-students' ? 'bg-white shadow text-orange-600' : 'text-slate-500 hover:bg-slate-200'}`}
          >
            <UserPlus size={18} /> 名單設定
          </button>
          <button
            onClick={() => setView('manage-tasks')}
            className={`px-4 py-2 rounded-xl font-bold transition-all text-sm sm:text-base flex items-center gap-1 ${view === 'manage-tasks' ? 'bg-white shadow text-pink-600' : 'text-slate-500 hover:bg-slate-200'}`}
          >
            <ClipboardList size={18} /> 任務設定
          </button>
        </div>
      </header>

      {/* 主內容區 */}
      <main className="max-w-6xl mx-auto p-6">
        {view === 'teacher' && <TeacherDashboard students={students} tasks={tasks} />}
        {view === 'student' && <StudentDashboard students={students} tasks={tasks} onToggleTask={toggleTask} />}
        {view === 'wheel' && <WheelOfFortune students={students} />}
        {view === 'manage-students' && <ManageStudents students={students} setStudents={setStudents} />}
        {view === 'manage-tasks' && <ManageTasks tasks={tasks} setTasks={setTasks} students={students} />}
      </main>
    </div>
  );
}

// ==========================================
// 模組 1: 老師儀表板 (Teacher Dashboard)
// ==========================================
function TeacherDashboard({ students, tasks }) {
  const [expandedId, setExpandedId] = useState(null);
  const [viewMode, setViewMode] = useState('grid'); // 'grid' | 'table'

  // 計算每個學生的任務狀態
  const studentStatus = students.map(student => {
    const studentTasks = tasks.filter(t => t.studentId === student.id);
    const total = studentTasks.length;
    const completed = studentTasks.filter(t => t.completed).length;
    let status = 'none'; // 無任務
    if (total > 0) {
      status = completed === total ? 'done' : 'pending';
    }
    if (student.absent) status = 'absent';

    return { ...student, tasks: studentTasks, status, total, completed };
  });

  return (
    <div className="animate-in fade-in duration-500">
      <div className="flex flex-col md:flex-row justify-between items-start md:items-end mb-6 gap-4">
        <div>
          <h2 className="text-3xl font-extrabold text-slate-800 mb-2">幹部任務狀態</h2>
          <p className="text-slate-500">一眼掌握今日全班幹部的做事進度!</p>
        </div>
        
        <div className="flex flex-col sm:flex-row gap-3 items-start sm:items-center">
          {/* 圖例 */}
          <div className="flex gap-4 text-sm font-bold bg-white p-3 rounded-2xl shadow-sm border border-slate-100">
            <div className="flex items-center gap-2"><span className="w-4 h-4 rounded-full bg-green-500"></span> 已完成</div>
            <div className="flex items-center gap-2"><span className="w-4 h-4 rounded-full bg-red-400"></span> 未完成</div>
            <div className="flex items-center gap-2"><span className="w-4 h-4 rounded-full bg-slate-300"></span> 無任務/請假</div>
          </div>

          {/* 視圖切換按鈕 */}
          <div className="flex bg-white p-1 rounded-2xl shadow-sm border border-slate-100">
            <button
              onClick={() => setViewMode('grid')}
              className={`p-2 rounded-xl transition-all flex items-center justify-center ${viewMode === 'grid' ? 'bg-blue-50 text-blue-600' : 'text-slate-400 hover:bg-slate-50'}`}
              title="卡片模式"
            >
              <LayoutGrid size={20} />
            </button>
            <button
              onClick={() => setViewMode('table')}
              className={`p-2 rounded-xl transition-all flex items-center justify-center ${viewMode === 'table' ? 'bg-blue-50 text-blue-600' : 'text-slate-400 hover:bg-slate-50'}`}
              title="表格模式"
            >
              <List size={20} />
            </button>
          </div>
        </div>
      </div>

      {viewMode === 'grid' ? (
        /* 卡片檢視模式 (Grid View) */
        <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
          {studentStatus.map(student => (
            <div 
              key={student.id} 
              className={`bg-white rounded-3xl p-5 shadow-sm border-2 transition-all ${
                student.status === 'pending' ? 'border-red-100 hover:border-red-200 hover:shadow-md' : 
                student.status === 'done' ? 'border-green-100' : 'border-slate-100'
              }`}
            >
              <div className="flex justify-between items-start mb-3">
                <div className="flex items-center gap-3">
                  <div className="text-4xl bg-slate-100 rounded-full p-2">{student.avatar}</div>
                  <div>
                    <h3 className="text-xl font-bold text-slate-800">{student.name}</h3>
                    <p className="text-sm font-bold text-slate-500 bg-slate-100 px-2 py-0.5 rounded-lg inline-block mt-1">
                      {student.role}
                    </p>
                  </div>
                </div>
                {/* 紅綠燈指示器 */}
                <div className={`w-6 h-6 rounded-full flex items-center justify-center ${
                  student.status === 'done' ? 'bg-green-500 text-white shadow-inner' :
                  student.status === 'pending' ? 'bg-red-400 animate-pulse text-white' :
                  'bg-slate-200'
                }`}>
                  {student.status === 'done' && <Check size={14} strokeWidth={4} />}
                  {student.status === 'pending' && <AlertCircle size={14} strokeWidth={3} />}
                </div>
              </div>

              {student.status === 'absent' ? (
                <div className="mt-4 p-3 bg-slate-50 rounded-xl text-center text-slate-400 font-bold">
                  今日請假
                </div>
              ) : student.total > 0 ? (
                <div className="mt-4">
                  <div className="flex justify-between text-sm font-bold text-slate-500 mb-1">
                    <span>進度: {student.completed}/{student.total}</span>
                    <span>{Math.round((student.completed / student.total) * 100)}%</span>
                  </div>
                  <div className="w-full bg-slate-100 rounded-full h-3 mb-3 overflow-hidden">
                    <div 
                      className={`h-3 rounded-full transition-all duration-1000 ${student.status === 'done' ? 'bg-green-500' : 'bg-red-400'}`}
                      style={{ width: `${(student.completed / student.total) * 100}%` }}
                    ></div>
                  </div>

                  {student.status === 'pending' && (
                    <button 
                      onClick={() => setExpandedId(expandedId === student.id ? null : student.id)}
                      className="w-full text-center py-2 text-sm font-bold text-blue-500 hover:bg-blue-50 rounded-xl transition-colors flex items-center justify-center gap-1"
                    >
                      {expandedId === student.id ? '收起明細' : '查看缺漏任務'}
                      {expandedId === student.id ? <ChevronUp size={16} /> : <ChevronDown size={16} />}
                    </button>
                  )}

                  {/* 展開未完成任務明細 */}
                  {expandedId === student.id && student.status === 'pending' && (
                    <ul className="mt-2 space-y-2 animate-in slide-in-from-top-2">
                      {student.tasks.filter(t => !t.completed).map(task => (
                        <li key={task.id} className="text-sm bg-red-50 text-red-700 p-2 rounded-lg flex items-start gap-2">
                          <AlertCircle size={16} className="shrink-0 mt-0.5" />
                          <span>{task.title}</span>
                        </li>
                      ))}
                    </ul>
                  )}
                </div>
              ) : (
                <div className="mt-4 p-3 bg-slate-50 rounded-xl text-center text-slate-400 font-bold">
                  無指派任務
                </div>
              )}
            </div>
          ))}
        </div>
      ) : (
        /* 表格檢視模式 (Table View) */
        <div className="bg-white rounded-3xl shadow-sm border border-slate-200 overflow-x-auto animate-in slide-in-from-bottom-2">
          <table className="w-full text-left border-collapse min-w-[800px]">
            <thead>
              <tr className="bg-slate-50 border-b border-slate-200 text-slate-500 text-sm">
                <th className="py-4 px-6 font-bold w-20 text-center">座號</th>
                <th className="py-4 px-6 font-bold w-48">學生</th>
                <th className="py-4 px-6 font-bold w-32">職位</th>
                <th className="py-4 px-6 font-bold w-32 text-center">狀態</th>
                <th className="py-4 px-6 font-bold">任務進度與缺漏</th>
              </tr>
            </thead>
            <tbody className="divide-y divide-slate-100">
              {studentStatus.map(student => (
                <tr key={student.id} className="hover:bg-slate-50 transition-colors">
                  <td className="py-3 px-6 text-center text-slate-500 font-bold">{student.id}</td>
                  <td className="py-3 px-6">
                    <div className="flex items-center gap-3">
                      <span className="text-2xl">{student.avatar}</span>
                      <span className="font-bold text-slate-800 text-lg">{student.name}</span>
                    </div>
                  </td>
                  <td className="py-3 px-6">
                    <span className="text-sm font-bold text-slate-600 bg-slate-100 px-2 py-1 rounded-lg">
                      {student.role}
                    </span>
                  </td>
                  <td className="py-3 px-6 text-center">
                    {student.status === 'done' && <span className="inline-flex items-center gap-1 bg-green-100 text-green-700 px-3 py-1 rounded-full text-sm font-bold"><CheckCircle2 size={16}/> 已完成</span>}
                    {student.status === 'pending' && <span className="inline-flex items-center gap-1 bg-red-100 text-red-600 px-3 py-1 rounded-full text-sm font-bold animate-pulse"><AlertCircle size={16}/> 未完成</span>}
                    {student.status === 'absent' && <span className="inline-flex items-center gap-1 bg-amber-100 text-amber-700 px-3 py-1 rounded-full text-sm font-bold">已請假</span>}
                    {student.status === 'none' && <span className="inline-flex items-center gap-1 bg-slate-100 text-slate-500 px-3 py-1 rounded-full text-sm font-bold">無任務</span>}
                  </td>
                  <td className="py-3 px-6">
                    {student.status === 'absent' || student.status === 'none' ? (
                      <span className="text-slate-400 font-bold">-</span>
                    ) : (
                      <div className="flex items-center gap-4">
                        <div className="flex items-center gap-2 min-w-[60px]">
                          <span className="text-sm font-bold text-slate-700">{student.completed}/{student.total}</span>
                        </div>
                        {/* 顯示未完成的任務標籤 */}
                        {student.status === 'pending' && (
                          <div className="flex flex-wrap gap-2">
                            {student.tasks.filter(t => !t.completed).map(task => (
                              <span key={task.id} className="bg-red-50 border border-red-100 text-red-600 text-xs px-2 py-1 rounded-md font-bold flex items-center gap-1">
                                <X size={12} /> {task.title}
                              </span>
                            ))}
                          </div>
                        )}
                        {/* 顯示已完成狀態 */}
                        {student.status === 'done' && (
                          <span className="text-sm text-green-500 font-bold flex items-center gap-1">
                            <Check size={16} /> 任務皆已完成
                          </span>
                        )}
                      </div>
                    )}
                  </td>
                </tr>
              ))}
            </tbody>
          </table>
        </div>
      )}
    </div>
  );
}

// ==========================================
// 模組 2: 學生打卡區 (Student Dashboard)
// ==========================================
function StudentDashboard({ students, tasks, onToggleTask }) {
  // 建立包含學生資訊的任務列表
  const enrichedTasks = tasks.map(task => {
    const student = students.find(s => s.id === task.studentId);
    return { ...task, student };
  }).filter(t => !t.student?.absent); // 排除請假的學生任務

  return (
    <div className="animate-in fade-in duration-500 max-w-3xl mx-auto">
      <div className="text-center mb-8">
        <h2 className="text-4xl font-extrabold text-slate-800 mb-3">任務打卡看板</h2>
        <p className="text-lg text-slate-500">做完自己的工作了嗎?點擊圓圈完成打卡!</p>
      </div>

      <div className="space-y-4">
        {enrichedTasks.map(task => (
          <div 
            key={task.id}
            onClick={() => onToggleTask(task.id)}
            className={`flex items-center p-4 sm:p-6 rounded-3xl border-2 cursor-pointer transition-all duration-300 transform hover:scale-[1.01] ${
              task.completed 
                ? 'bg-slate-50 border-slate-200 opacity-60' 
                : 'bg-white border-blue-100 shadow-sm hover:shadow-md hover:border-blue-300'
            }`}
          >
            {/* 打勾按鈕區 */}
            <div className="shrink-0 mr-4 sm:mr-6">
              <button className={`w-10 h-10 sm:w-14 sm:h-14 rounded-full flex items-center justify-center transition-all duration-300 ${
                task.completed ? 'bg-green-500 scale-110' : 'bg-slate-100 hover:bg-slate-200'
              }`}>
                {task.completed ? (
                  <CheckCircle2 className="text-white w-8 h-8 sm:w-10 sm:h-10 animate-in zoom-in" />
                ) : (
                  <Circle className="text-slate-300 w-8 h-8 sm:w-10 sm:h-10" />
                )}
              </button>
            </div>

            {/* 內容區 */}
            <div className="flex-1">
              <div className="flex items-center gap-2 mb-1">
                <span className="text-2xl">{task.student?.avatar}</span>
                <span className={`font-bold text-sm sm:text-base px-2 py-0.5 rounded-lg ${task.completed ? 'bg-slate-200 text-slate-500' : 'bg-blue-100 text-blue-700'}`}>
                  {task.student?.role} - {task.student?.name}
                </span>
                <span className="text-xs font-bold text-slate-400 border border-slate-200 px-2 py-0.5 rounded-full hidden sm:inline-block">
                  {task.freq}
                </span>
              </div>
              <h3 className={`text-xl sm:text-2xl font-bold transition-all duration-300 ${
                task.completed ? 'line-through text-slate-400' : 'text-slate-800'
              }`}>
                {task.title}
              </h3>
            </div>
          </div>
        ))}
      </div>
    </div>
  );
}

// ==========================================
// 模組 3: 命運轉盤 (Random Draw Module)
// ==========================================
function WheelOfFortune({ students }) {
  const [taskName, setTaskName] = useState('去學務處搬器材');
  const [excludeAbsent, setExcludeAbsent] = useState(true);
  const [excludeBusy, setExcludeBusy] = useState(true);
  const [isSpinning, setIsSpinning] = useState(false);
  const [rotation, setRotation] = useState(0);
  const [winner, setWinner] = useState(null);
  
  const wheelRef = useRef(null);

  // 決定候選人名單
  const candidates = students.filter(s => {
    if (excludeAbsent && s.absent) return false;
    if (excludeBusy && s.role !== '無') return false;
    return true;
  });

  const numSlices = candidates.length;
  const sliceAngle = 360 / numSlices;

  // 產生轉盤背景色 (CSS conic-gradient)
  const getGradient = () => {
    if (numSlices === 0) return 'gray';
    let gradient = 'conic-gradient(';
    for (let i = 0; i < numSlices; i++) {
      const color = colors[i % colors.length];
      gradient += `${color} ${i * sliceAngle}deg ${(i + 1) * sliceAngle}deg`;
      if (i < numSlices - 1) gradient += ', ';
    }
    gradient += ')';
    return gradient;
  };

  const spinWheel = () => {
    if (isSpinning || numSlices === 0) return;
    setIsSpinning(true);
    setWinner(null);

    // 隨機旋轉角度:至少轉 5 圈 (1800度) + 隨機角度
    const spinDegrees = 1800 + Math.random() * 360;
    const newRotation = rotation + spinDegrees;
    setRotation(newRotation);

    // 播放模擬音效
    const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
    const playTick = (time) => {
       const osc = audioCtx.createOscillator();
       osc.frequency.value = 800;
       osc.connect(audioCtx.destination);
       osc.start(time);
       osc.stop(time + 0.02);
    };
    
    // 簡單的音效節奏
    for(let i=0; i<15; i++) {
        try { playTick(audioCtx.currentTime + i * 0.2); } catch(e){}
    }

    // 動畫結束後計算得獎者 (CSS transition 設定為 4 秒)
    setTimeout(() => {
      setIsSpinning(false);
      // 計算最終停在哪個區塊。
      // 指針固定在最上方 (即 0 度)。
      // 輪盤順時針轉了 newRotation 度,這表示畫面上 0 度的位置,對應到輪盤內部原本的 360 - (newRotation % 360) 度。
      const normalizedRotation = newRotation % 360;
      // 因為 slice 是從 0 度開始畫,所以我們要找哪個 slice 包含了 360 - normalizedRotation。
      // 修正偏移:因為我們繪製文字時讓文字置中在 slice 中間,且可能存在偏移。
      // 最簡單的算法:
      const actualDeg = (360 - normalizedRotation) % 360;
      let winningIndex = Math.floor(actualDeg / sliceAngle);
      
      setWinner(candidates[winningIndex]);
      
      // 歡呼音效
      try {
        const osc = audioCtx.createOscillator();
        osc.frequency.setValueAtTime(400, audioCtx.currentTime);
        osc.frequency.exponentialRampToValueAtTime(1200, audioCtx.currentTime + 0.5);
        osc.connect(audioCtx.destination);
        osc.start();
        osc.stop(audioCtx.currentTime + 0.5);
      } catch(e){}

    }, 4000); // 對應 CSS 的 duration-4000
  };

  return (
    <div className="animate-in fade-in duration-500 max-w-4xl mx-auto flex flex-col md:flex-row gap-8 items-center">
      {/* 左側:設定區 */}
      <div className="w-full md:w-1/3 bg-white p-6 rounded-3xl shadow-sm border border-slate-100 flex flex-col gap-6">
        <div>
          <h2 className="text-3xl font-extrabold text-slate-800 mb-2">命運轉盤</h2>
          <p className="text-slate-500 font-bold">誰是今天的幸運兒?</p>
        </div>

        <div className="space-y-4">
          <div>
            <label className="block text-sm font-bold text-slate-500 mb-2">臨時任務名稱</label>
            <input 
              type="text" 
              value={taskName}
              onChange={(e) => setTaskName(e.target.value)}
              className="w-full bg-slate-50 border-2 border-slate-200 rounded-xl px-4 py-3 font-bold text-slate-800 focus:outline-none focus:border-purple-400 transition-colors"
              placeholder="例如:倒垃圾、搬課本..."
            />
          </div>

          <div className="bg-slate-50 p-4 rounded-2xl space-y-3 border border-slate-100">
            <h3 className="font-bold text-slate-700 mb-2 flex items-center gap-2"><Settings2 size={18}/> 篩選條件</h3>
            <label className="flex items-center gap-3 cursor-pointer">
              <input 
                type="checkbox" 
                checked={excludeAbsent} 
                onChange={(e) => setExcludeAbsent(e.target.checked)}
                className="w-5 h-5 rounded text-purple-600 focus:ring-purple-500"
              />
              <span className="font-bold text-slate-600">排除「請假未到」</span>
            </label>
            <label className="flex items-center gap-3 cursor-pointer">
              <input 
                type="checkbox" 
                checked={excludeBusy} 
                onChange={(e) => setExcludeBusy(e.target.checked)}
                className="w-5 h-5 rounded text-purple-600 focus:ring-purple-500"
              />
              <span className="font-bold text-slate-600">排除「已有職務者」</span>
            </label>
          </div>

          <div className="text-sm font-bold text-purple-600 bg-purple-50 p-3 rounded-xl text-center">
            目前符合條件人數:{numSlices} 人
          </div>
        </div>

        <button 
          onClick={spinWheel}
          disabled={isSpinning || numSlices === 0}
          className={`mt-auto w-full py-4 rounded-2xl font-extrabold text-xl transition-all transform active:scale-95 ${
            isSpinning || numSlices === 0 
              ? 'bg-slate-200 text-slate-400 cursor-not-allowed' 
              : 'bg-gradient-to-r from-purple-500 to-pink-500 text-white shadow-lg hover:shadow-xl hover:from-purple-400 hover:to-pink-400'
          }`}
        >
          {isSpinning ? '轉動中...' : '開始抽取!'}
        </button>
      </div>

      {/* 右側:轉盤區 */}
      <div className="w-full md:w-2/3 flex justify-center items-center relative py-10">
        
        {/* 指針 (置頂) */}
        <div className="absolute top-4 left-1/2 -translate-x-1/2 z-10 drop-shadow-lg">
          <div className="w-0 h-0 border-l-[20px] border-r-[20px] border-t-[40px] border-l-transparent border-r-transparent border-t-slate-800"></div>
        </div>

        {/* 轉盤本體 */}
        <div className="relative w-80 h-80 sm:w-96 sm:h-96">
          {numSlices > 0 ? (
             <div 
             ref={wheelRef}
             className="w-full h-full rounded-full border-8 border-white shadow-2xl relative overflow-hidden"
             style={{ 
               background: getGradient(),
               transform: `rotate(${rotation}deg)`,
               transition: 'transform 4s cubic-bezier(0.15, 0.85, 0.35, 1)' 
             }}
           >
             {candidates.map((candidate, index) => {
               // 計算每個名字在輪盤上的旋轉角度
               const textRotation = (index * sliceAngle) + (sliceAngle / 2);
               return (
                 <div 
                   key={candidate.id}
                   className="absolute w-full h-full top-0 left-0 flex items-start justify-center pt-8"
                   style={{ transform: `rotate(${textRotation}deg)` }}
                 >
                   <span className="font-black text-xl text-slate-800 drop-shadow-md whitespace-nowrap" style={{ transform: 'rotate(90deg)', transformOrigin: 'center 40px' }}>
                     {candidate.avatar} {candidate.name}
                   </span>
                 </div>
               );
             })}
           </div>
          ) : (
            <div className="w-full h-full rounded-full border-8 border-white shadow-xl bg-slate-100 flex items-center justify-center text-slate-400 font-bold text-xl">
              無人符合條件
            </div>
          )}
         
          {/* 中心圓點 */}
          <div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-16 h-16 bg-white rounded-full shadow-lg border-4 border-purple-100 flex items-center justify-center">
            <span className="text-2xl">🎯</span>
          </div>
        </div>
      </div>

      {/* 恭喜中獎彈窗 (Modal) */}
      {winner && (
        <div className="fixed inset-0 bg-slate-900/40 backdrop-blur-sm flex items-center justify-center z-50 p-4 animate-in fade-in">
          <div className="bg-white rounded-3xl p-8 max-w-sm w-full text-center shadow-2xl animate-in zoom-in-95 bounce">
            <div className="text-6xl mb-4 animate-bounce">{winner.avatar}</div>
            <h3 className="text-3xl font-extrabold text-slate-800 mb-2">恭喜 {winner.name}!</h3>
            <p className="text-lg font-bold text-purple-600 mb-6 bg-purple-50 py-3 rounded-xl border border-purple-100">
              獲得任務:<br/>「{taskName}」
            </p>
            <button 
              onClick={() => setWinner(null)}
              className="w-full bg-slate-800 text-white font-bold py-3 rounded-xl hover:bg-slate-700 transition-colors"
            >
              確定
            </button>
          </div>
        </div>
      )}
    </div>
  );
}

// ==========================================
// 模組 4: 學生名單管理 (Manage Students)
// ==========================================
function ManageStudents({ students, setStudents }) {
  const [newName, setNewName] = useState('');
  const [newAvatar, setNewAvatar] = useState('👦🏻');
  const [newRole, setNewRole] = useState('無');

  const handleAddStudent = (e) => {
    e.preventDefault();
    if (!newName.trim()) return;
    const newId = students.length > 0 ? Math.max(...students.map(s => s.id)) + 1 : 1;
    setStudents([...students, { id: newId, name: newName, avatar: newAvatar, role: newRole, absent: false }]);
    setNewName('');
    setNewRole('無');
  };

  const handleDelete = (id) => {
    setStudents(students.filter(s => s.id !== id));
  };

  const toggleAbsent = (id) => {
    setStudents(students.map(s => s.id === id ? { ...s, absent: !s.absent } : s));
  };

  return (
    <div className="animate-in fade-in duration-500 max-w-4xl mx-auto">
      <div className="mb-8">
        <h2 className="text-3xl font-extrabold text-slate-800 mb-2">班級名單設定</h2>
        <p className="text-slate-500">在這裡新增學生、指派幹部職位,或標記請假狀態。</p>
      </div>

      {/* 新增學生表單 */}
      <form onSubmit={handleAddStudent} className="bg-white p-6 rounded-3xl shadow-sm border border-slate-100 flex flex-col sm:flex-row gap-4 mb-8 items-end">
        <div className="w-full sm:w-1/4">
          <label className="block text-sm font-bold text-slate-500 mb-2">頭像 (Emoji)</label>
          <input type="text" value={newAvatar} onChange={(e) => setNewAvatar(e.target.value)} className="w-full bg-slate-50 border-2 border-slate-200 rounded-xl px-4 py-2 font-bold focus:border-orange-400 focus:outline-none" />
        </div>
        <div className="w-full sm:w-1/3">
          <label className="block text-sm font-bold text-slate-500 mb-2">姓名</label>
          <input type="text" placeholder="例如:林小華" value={newName} onChange={(e) => setNewName(e.target.value)} className="w-full bg-slate-50 border-2 border-slate-200 rounded-xl px-4 py-2 font-bold focus:border-orange-400 focus:outline-none" />
        </div>
        <div className="w-full sm:w-1/3">
          <label className="block text-sm font-bold text-slate-500 mb-2">幹部職位</label>
          <input type="text" placeholder="例如:班長、無" value={newRole} onChange={(e) => setNewRole(e.target.value)} className="w-full bg-slate-50 border-2 border-slate-200 rounded-xl px-4 py-2 font-bold focus:border-orange-400 focus:outline-none" />
        </div>
        <button type="submit" className="w-full sm:w-auto px-6 py-2.5 bg-orange-500 hover:bg-orange-600 text-white font-bold rounded-xl transition-colors flex items-center justify-center gap-2">
          <Plus size={20} /> 新增
        </button>
      </form>

      {/* 學生名單列表 */}
      <div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
        {students.map(student => (
          <div key={student.id} className={`bg-white p-4 rounded-2xl border-2 flex items-center justify-between transition-all ${student.absent ? 'border-slate-200 opacity-60 bg-slate-50' : 'border-slate-100 shadow-sm'}`}>
            <div className="flex items-center gap-3">
              <div className="text-3xl">{student.avatar}</div>
              <div>
                <div className="font-bold text-slate-800">{student.name}</div>
                <div className="text-xs font-bold text-slate-500 bg-slate-100 px-2 py-0.5 rounded-md inline-block mt-1">{student.role}</div>
              </div>
            </div>
            <div className="flex gap-2">
              <button onClick={() => toggleAbsent(student.id)} className={`px-2 py-1 text-xs font-bold rounded-lg transition-colors ${student.absent ? 'bg-amber-100 text-amber-700' : 'bg-slate-100 text-slate-500 hover:bg-slate-200'}`}>
                {student.absent ? '已請假' : '設為請假'}
              </button>
              <button onClick={() => handleDelete(student.id)} className="p-1.5 text-red-400 hover:bg-red-50 hover:text-red-600 rounded-lg transition-colors">
                <Trash2 size={18} />
              </button>
            </div>
          </div>
        ))}
      </div>
    </div>
  );
}

// ==========================================
// 模組 5: 任務清單管理 (Manage Tasks)
// ==========================================
function ManageTasks({ tasks, setTasks, students }) {
  const [newTitle, setNewTitle] = useState('');
  const [newStudentId, setNewStudentId] = useState('');
  const [newFreq, setNewFreq] = useState('每日');
  const [startTime, setStartTime] = useState('08:00');
  const [endTime, setEndTime] = useState('08:30');

  const handleAddTask = (e) => {
    e.preventDefault();
    if (!newTitle.trim() || !newStudentId) return;
    const newId = tasks.length > 0 ? Math.max(...tasks.map(t => t.id)) + 1 : 1;
    
    // 如果選擇自訂時間,將頻率文字轉換為具體時間段
    const finalFreq = newFreq === '自訂時間' ? `${startTime} ~ ${endTime}` : newFreq;
    
    setTasks([...tasks, { 
      id: newId, 
      title: newTitle, 
      studentId: Number(newStudentId), 
      freq: finalFreq, 
      completed: false 
    }]);
    setNewTitle('');
  };

  const handleDelete = (id) => {
    setTasks(tasks.filter(t => t.id !== id));
  };

  return (
    <div className="animate-in fade-in duration-500 max-w-4xl mx-auto">
      <div className="mb-8">
        <h2 className="text-3xl font-extrabold text-slate-800 mb-2">常態任務設定</h2>
        <p className="text-slate-500">建立班級的例行公事,並分配給指定的學生或幹部。</p>
      </div>

      {/* 新增任務表單 */}
      <form onSubmit={handleAddTask} className="bg-white p-6 rounded-3xl shadow-sm border border-slate-100 flex flex-col gap-4 mb-8">
        <div className="flex flex-col md:flex-row gap-4 items-end">
          <div className="w-full md:w-2/5">
            <label className="block text-sm font-bold text-slate-500 mb-2">任務內容</label>
            <input type="text" placeholder="例如:擦黑板、倒垃圾..." value={newTitle} onChange={(e) => setNewTitle(e.target.value)} className="w-full bg-slate-50 border-2 border-slate-200 rounded-xl px-4 py-2 font-bold focus:border-pink-400 focus:outline-none" />
          </div>
          <div className="w-full md:w-1/4">
            <label className="block text-sm font-bold text-slate-500 mb-2">負責學生</label>
            <select value={newStudentId} onChange={(e) => setNewStudentId(e.target.value)} className="w-full bg-slate-50 border-2 border-slate-200 rounded-xl px-4 py-2 font-bold text-slate-700 focus:border-pink-400 focus:outline-none appearance-none">
              <option value="" disabled>請選擇...</option>
              {students.map(s => (
                <option key={s.id} value={s.id}>{s.name} ({s.role})</option>
              ))}
            </select>
          </div>
          <div className="w-full md:w-1/4">
            <label className="block text-sm font-bold text-slate-500 mb-2">執行頻率</label>
            <select value={newFreq} onChange={(e) => setNewFreq(e.target.value)} className="w-full bg-slate-50 border-2 border-slate-200 rounded-xl px-4 py-2 font-bold text-slate-700 focus:border-pink-400 focus:outline-none appearance-none">
              <option value="每日">每日</option>
              <option value="每週">每週</option>
              <option value="早自修">早自修</option>
              <option value="午休">午休</option>
              <option value="自訂時間">自訂時間...</option>
              <option value="單次">單次</option>
            </select>
          </div>
          <button type="submit" className="w-full md:w-auto px-6 py-2.5 bg-pink-500 hover:bg-pink-600 text-white font-bold rounded-xl transition-colors flex items-center justify-center gap-2">
            <Plus size={20} /> 新增
          </button>
        </div>

        {/* 條件渲染:自訂時間選取器 */}
        {newFreq === '自訂時間' && (
          <div className="flex items-center gap-3 animate-in fade-in slide-in-from-top-2 pt-2 border-t border-slate-100">
            <div className="flex items-center gap-2 bg-slate-50 border-2 border-slate-200 rounded-xl px-4 py-2">
              <span className="text-sm font-bold text-slate-500">從</span>
              <input type="time" value={startTime} onChange={e => setStartTime(e.target.value)} className="bg-transparent font-bold text-slate-700 focus:outline-none" />
            </div>
            <span className="font-bold text-slate-300">~</span>
            <div className="flex items-center gap-2 bg-slate-50 border-2 border-slate-200 rounded-xl px-4 py-2">
              <span className="text-sm font-bold text-slate-500">到</span>
              <input type="time" value={endTime} onChange={e => setEndTime(e.target.value)} className="bg-transparent font-bold text-slate-700 focus:outline-none" />
            </div>
          </div>
        )}
      </form>

      {/* 任務列表 */}
      <div className="bg-white rounded-3xl border border-slate-100 shadow-sm overflow-hidden">
        {tasks.length === 0 ? (
          <div className="p-8 text-center text-slate-400 font-bold">目前沒有任何任務</div>
        ) : (
          <div className="divide-y divide-slate-100">
            {tasks.map(task => {
              const assignedStudent = students.find(s => s.id === task.studentId);
              return (
                <div key={task.id} className="p-4 sm:p-5 flex items-center justify-between hover:bg-slate-50 transition-colors">
                  <div className="flex items-center gap-4">
                    <div className="w-10 h-10 rounded-full bg-pink-100 flex items-center justify-center text-pink-500 shrink-0">
                      <ClipboardList size={20} />
                    </div>
                    <div>
                      <h4 className="font-bold text-slate-800 sm:text-lg">{task.title}</h4>
                      <div className="flex items-center gap-2 mt-1">
                        <span className="text-xs font-bold text-slate-500 border border-slate-200 px-2 py-0.5 rounded-md">
                          {task.freq}
                        </span>
                        {assignedStudent ? (
                          <span className="text-sm text-slate-500">
                            負責人: <span className="font-bold text-slate-700">{assignedStudent.name}</span>
                          </span>
                        ) : (
                          <span className="text-sm text-red-400 font-bold">負責人已被刪除</span>
                        )}
                      </div>
                    </div>
                  </div>
                  <button onClick={() => handleDelete(task.id)} className="p-2 text-red-400 hover:bg-red-50 hover:text-red-600 rounded-xl transition-colors shrink-0">
                    <Trash2 size={20} />
                  </button>
                </div>
              );
            })}
          </div>
        )}
      </div>
    </div>
  );
}
第二個程式碼
import React, { useState, useEffect } from 'react';
import { 
  Users, LayoutGrid, ListChecks, Award, AlertCircle, 
  X, Check, Clock, ChevronRight, UserCircle2, BookOpen,
  MessageSquare, Smartphone, Settings, Plus, Trash2, 
  Calendar, CheckCircle2, ClipboardList, LayoutList
} from 'lucide-react';

// --- Mock Data ---
const INITIAL_STUDENTS = [
  { id: 's1', no: 1, name: '王小明', points: 12, avatar: '👦🏻' },
  { id: 's2', no: 2, name: '陳小華', points: 8, avatar: '👧🏻' },
  { id: 's3', no: 3, name: '林雅婷', points: 15, avatar: '👧🏽' },
  { id: 's4', no: 4, name: '張建國', points: 5, avatar: '👦🏽' },
  { id: 's5', no: 5, name: '吳美麗', points: 20, avatar: '👧🏻' },
  { id: 's6', no: 6, name: '李志明', points: -2, avatar: '👦🏻' },
  { id: 's7', no: 7, name: '周杰倫', points: 10, avatar: '👦🏼' },
  { id: 's8', no: 8, name: '蔡依林', points: 18, avatar: '👧🏼' },
];

const INITIAL_ASSIGNMENTS = [
  { id: 'a1', name: '國語習作 CH5', subject: '國語課' },
  { id: 'a2', name: '數學演練 P.20-21', subject: '數學' },
  { id: 'a3', name: '家庭聯絡簿', subject: '其他' },
];

const BEHAVIOR_TAGS = [
  { id: 't1', name: '踴躍發言', type: 'positive', context: ['國語課', '綜合活動'] },
  { id: 't2', name: '干擾上課', type: 'needs_improvement', context: ['國語課', '綜合活動', '科任課', '通用'] },
  { id: 't3', name: '專心聽講', type: 'positive', context: ['國語課', '科任課', '通用'] },
  { id: 't4', name: '遲到', type: 'needs_improvement', context: ['早自修'] },
  { id: 't5', name: '主動打掃', type: 'positive', context: ['早自修', '打掃'] },
  { id: 't6', name: '未交聯絡簿', type: 'needs_improvement', context: ['早自修'] },
  { id: 't7', name: '團隊合作佳', type: 'positive', context: ['綜合活動', '通用'] },
  { id: 't8', name: '作業優良', type: 'positive', context: ['科任課', '通用'] },
  { id: 't9', name: '忘記帶學具', type: 'needs_improvement', context: ['科任課', '通用'] },
  { id: 't10', name: '安靜午休', type: 'positive', context: ['午休'] },
  { id: 't11', name: '走廊奔跑', type: 'needs_improvement', context: ['下課', '放學'] },
  { id: 't12', name: '餐具未收', type: 'needs_improvement', context: ['午餐'] },
];

const INITIAL_CONTEXTS = [
  '早自修', '國語課', '數學', '英文', '生活', 
  '自然', '社會', '音樂', '體育', '綜合活動', 
  '彈性', '午餐', '午休', '下課', '放學', '打掃', '其他'
];

const ROUTINES = ['早自修', '午餐', '午休', '下課', '放學', '打掃', '其他'];

// Helper: Get today's date string YYYY-MM-DD
const getTodayString = () => {
  const tzoffset = (new Date()).getTimezoneOffset() * 60000;
  return (new Date(Date.now() - tzoffset)).toISOString().split('T')[0];
};

export default function App() {
  // --- Global State ---
  const [view, setView] = useState('teacher'); // 'teacher' | 'collaborator'
  const [students, setStudents] = useState(INITIAL_STUDENTS);
  const [contexts, setContexts] = useState(INITIAL_CONTEXTS);
  const [assignments, setAssignments] = useState(INITIAL_ASSIGNMENTS);
  const todayStr = getTodayString();
  
  // Timeline Records
  const [records, setRecords] = useState([
    { id: 'r1', studentId: 's3', type: 'tag', tagName: '主動打掃', tagType: 'positive', context: '早自修', time: '07:50', recorder: '導師' },
    { id: 'r2', studentId: 's6', type: 'tag', tagName: '遲到', tagType: 'needs_improvement', context: '早自修', time: '08:05', recorder: '導師' }
  ]);

  // Missing Records State
  const [missingRecords, setMissingRecords] = useState([
    { id: 'm1', studentId: 's2', assignmentId: null, name: '生字乙本', subject: '國語課', date: '2026-03-17' },
    { id: 'm2', studentId: 's6', assignmentId: null, name: '自然觀察紀錄表', subject: '自然', date: '2026-03-18' },
  ]);

  // --- UI State ---
  const [teacherViewMode, setTeacherViewMode] = useState('grid'); // 'grid' | 'summary'
  const [activeContext, setActiveContext] = useState(contexts[0]);
  const [selectedStudentId, setSelectedStudentId] = useState(null);
  const [showMissingTable, setShowMissingTable] = useState(false);
  
  // Student Panel Specific State
  const [studentPanelContext, setStudentPanelContext] = useState('');

  useEffect(() => {
    if (selectedStudentId) {
      setStudentPanelContext(activeContext);
    }
  }, [selectedStudentId, activeContext]);

  // Settings State
  const [showSettings, setShowSettings] = useState(false);
  const [settingsTab, setSettingsTab] = useState('students');
  const [newStudent, setNewStudent] = useState({ no: '', name: '', avatar: '👦🏻' });
  const [newContext, setNewContext] = useState('');
  const [newAssignment, setNewAssignment] = useState({ name: '', subject: '國語課' });

  // Manual Missing Record Form State
  const [manualMissing, setManualMissing] = useState({ date: todayStr, subject: '國語課', name: '' });

  // Collaborator State
  const [collabSelectedStudents, setCollabSelectedStudents] = useState([]);
  const [collabContext, setCollabContext] = useState('科任課');
  const [showToast, setShowToast] = useState(false);

  // --- Actions ---
  const addRecord = (studentId, type, content, isCollaborator = false, contextOverride = null) => {
    const contextToUse = contextOverride || (isCollaborator ? collabContext : activeContext);
    const newRecord = {
      id: Date.now().toString() + Math.random(),
      studentId,
      type,
      tagName: type === 'tag' ? content.name : null,
      tagType: type === 'tag' ? content.type : null,
      assignmentName: type === 'assignment' ? content.name : null,
      context: contextToUse,
      time: new Date().toLocaleTimeString('zh-TW', { hour: '2-digit', minute: '2-digit' }),
      recorder: isCollaborator ? `${collabContext}老師` : '導師'
    };

    setRecords(prev => [newRecord, ...prev]);

    if (type === 'tag') {
      setStudents(prev => prev.map(s => s.id === studentId ? { ...s, points: s.points + (content.type === 'positive' ? 1 : -1) } : s));
    }
  };

  const handleCollabSubmit = (tag) => {
    collabSelectedStudents.forEach(studentId => {
      addRecord(studentId, 'tag', tag, true);
    });
    setCollabSelectedStudents([]);
    setShowToast(true);
    setTimeout(() => setShowToast(false), 3000);
  };

  const handleCustomTag = (type, isCollaborator = false) => {
    const customText = prompt(`請輸入自訂${type === 'positive' ? '正向' : '待改進'}行為:`);
    if (customText && customText.trim()) {
      if (isCollaborator) {
        handleCollabSubmit({ id: `c_${Date.now()}`, name: customText.trim(), type });
      } else {
        addRecord(selectedStudentId, 'tag', { name: customText.trim(), type }, false, studentPanelContext);
      }
    }
  };

  // --- Missing Assignment Actions ---
  const isMissingToday = (studentId, assignmentId) => {
    return missingRecords.some(r => r.studentId === studentId && r.assignmentId === assignmentId && r.date === todayStr);
  };

  const toggleTodayAssignment = (studentId, assignment) => {
    const isMissing = isMissingToday(studentId, assignment.id);
    if (isMissing) {
      setMissingRecords(prev => prev.filter(r => !(r.studentId === studentId && r.assignmentId === assignment.id && r.date === todayStr)));
    } else {
      const newMissing = { id: Date.now().toString() + Math.random(), studentId, assignmentId: assignment.id, name: assignment.name, subject: assignment.subject, date: todayStr };
      setMissingRecords(prev => [...prev, newMissing]);
      addRecord(studentId, 'assignment', assignment, false, studentPanelContext);
    }
  };

  const handleAddManualMissing = (studentId) => {
    if (!manualMissing.name.trim() || !manualMissing.date) return;
    const newMissing = { id: Date.now().toString() + Math.random(), studentId, assignmentId: null, name: manualMissing.name.trim(), subject: manualMissing.subject, date: manualMissing.date };
    setMissingRecords(prev => [...prev, newMissing]);
    setManualMissing({ date: todayStr, subject: contexts[0] || '其他', name: '' });
  };

  const markMissingAsDone = (recordId) => {
    setMissingRecords(prev => prev.filter(r => r.id !== recordId));
  };


  // --- Settings Actions ---
  const handleAddStudent = () => {
    if (!newStudent.no || !newStudent.name) return;
    setStudents(prev => [...prev, { ...newStudent, id: `s${Date.now()}`, points: 0 }].sort((a, b) => a.no - b.no));
    setNewStudent({ no: '', name: '', avatar: '👦🏻' });
  };
  const handleDeleteStudent = (id) => { if(window.confirm('確定要刪除這位學生嗎?')) setStudents(prev => prev.filter(s => s.id !== id)); };
  
  const handleAddContext = () => {
    if (!newContext.trim() || contexts.includes(newContext.trim())) return;
    setContexts(prev => [...prev, newContext.trim()]);
    setNewContext('');
  };
  const handleDeleteContext = (ctx) => {
    if(window.confirm(`確定要刪除情境「${ctx}」嗎?`)) {
      setContexts(prev => prev.filter(c => c !== ctx));
      if (activeContext === ctx) setActiveContext(contexts[0] !== ctx ? contexts[0] : contexts[1]);
    }
  };
  
  const handleAddAssignment = () => {
    if (!newAssignment.name.trim()) return;
    setAssignments(prev => [...prev, { id: `a${Date.now()}`, name: newAssignment.name.trim(), subject: newAssignment.subject }]);
    setNewAssignment({ name: '', subject: contexts.includes('國語課') ? '國語課' : contexts[0] });
  };
  const handleDeleteAssignment = (id) => { if(window.confirm('確定要刪除這個作業項目嗎?')) setAssignments(prev => prev.filter(a => a.id !== id)); };

  // --- Derived Data ---
  const selectedStudent = students.find(s => s.id === selectedStudentId);
  const getTagsForContext = (ctx) => {
    const specificTags = BEHAVIOR_TAGS.filter(t => t.context.includes(ctx));
    return specificTags.length > 0 ? specificTags : BEHAVIOR_TAGS.filter(t => t.context.includes('通用'));
  };
  const contextTags = getTagsForContext(studentPanelContext || activeContext);
  const collabTags = getTagsForContext(collabContext);
  
  const routineContexts = contexts.filter(c => ROUTINES.includes(c));
  const subjectContexts = contexts.filter(c => !ROUTINES.includes(c));

  const missingSummaryByStudent = missingRecords.reduce((acc, record) => {
    if (!acc[record.studentId]) acc[record.studentId] = [];
    acc[record.studentId].push(record);
    return acc;
  }, {});

  // ==========================================
  // COMPONENT: Missing Assignments Table Modal
  // ==========================================
  const renderMissingTableModal = () => {
    const sortedRecords = [...missingRecords].sort((a, b) => {
      if (a.date !== b.date) return new Date(a.date) - new Date(b.date);
      const studentA = students.find(s => s.id === a.studentId)?.no || 999;
      const studentB = students.find(s => s.id === b.studentId)?.no || 999;
      return studentA - studentB;
    });

    return (
      <div className="fixed inset-0 z-[100] flex justify-center items-center bg-slate-900/60 backdrop-blur-sm p-4">
        <div className="bg-white rounded-2xl shadow-2xl w-full max-w-4xl max-h-[90vh] flex flex-col overflow-hidden animate-in fade-in zoom-in-95 duration-200">
          <div className="px-4 md:px-6 py-4 md:py-5 border-b border-slate-200 flex justify-between items-center bg-indigo-50">
            <h2 className="text-xl md:text-2xl font-bold flex items-center gap-2 text-indigo-900">
              <ClipboardList className="text-indigo-600" /> 缺交總表
            </h2>
            <button onClick={() => setShowMissingTable(false)} className="p-2 hover:bg-indigo-100 rounded-full text-indigo-400 hover:text-indigo-700 transition-colors"><X size={24} /></button>
          </div>
          
          <div className="p-4 md:p-6 flex-1 overflow-y-auto bg-slate-50">
            {sortedRecords.length > 0 ? (
              <div className="bg-white border border-slate-200 rounded-xl overflow-x-auto shadow-sm">
                <table className="w-full text-left text-sm whitespace-nowrap min-w-[600px]">
                  <thead className="bg-slate-100 border-b border-slate-200 text-slate-600 font-bold">
                    <tr><th className="px-4 md:px-6 py-4 w-24 text-center">日期</th><th className="px-4 md:px-6 py-4 w-32">學生</th><th className="px-4 md:px-6 py-4 w-32">科目</th><th className="px-4 md:px-6 py-4">作業名稱</th><th className="px-4 md:px-6 py-4 w-32 text-center">操作</th></tr>
                  </thead>
                  <tbody className="divide-y divide-slate-100">
                    {sortedRecords.map((record) => {
                      const student = students.find(s => s.id === record.studentId);
                      return (
                        <tr key={record.id} className="hover:bg-slate-50 transition-colors group">
                          <td className="px-4 md:px-6 py-4 text-center"><span className={`font-medium ${record.date === todayStr ? 'text-indigo-600' : 'text-slate-500'}`}>{record.date}</span></td>
                          <td className="px-4 md:px-6 py-4">{student ? <div className="flex items-center gap-2"><span className="text-xl">{student.avatar}</span><span className="font-bold text-slate-800">{student.no}. {student.name}</span></div> : <span className="text-slate-400">未知</span>}</td>
                          <td className="px-4 md:px-6 py-4"><span className="bg-slate-100 text-slate-600 px-2.5 py-1 rounded-md text-xs font-bold border border-slate-200">{record.subject}</span></td>
                          <td className="px-4 md:px-6 py-4 font-bold text-red-600">{record.name}</td>
                          <td className="px-4 md:px-6 py-4 text-center"><button onClick={() => markMissingAsDone(record.id)} className="inline-flex items-center gap-1.5 px-3 py-1.5 bg-green-50 hover:bg-green-500 text-green-700 hover:text-white border border-green-200 hover:border-green-500 rounded-lg text-sm font-bold transition-all"><CheckCircle2 size={16} /> <span className="hidden sm:inline">已完成</span></button></td>
                        </tr>
                      );
                    })}
                  </tbody>
                </table>
              </div>
            ) : (
              <div className="flex flex-col items-center justify-center h-40 md:h-64 text-slate-400 gap-4"><CheckCircle2 size={48} className="text-green-400" /><p className="text-base md:text-lg font-bold text-slate-600">目前全班沒有任何缺交紀錄。</p></div>
            )}
          </div>
        </div>
      </div>
    );
  };

  // ==========================================
  // COMPONENT: Settings Modal
  // ==========================================
  const renderSettingsModal = () => (
    <div className="fixed inset-0 z-[100] flex justify-center items-center bg-slate-900/40 backdrop-blur-sm p-4">
      <div className="bg-white rounded-2xl shadow-2xl w-full max-w-2xl max-h-[90vh] flex flex-col overflow-hidden animate-in fade-in zoom-in-95 duration-200">
        <div className="px-4 md:px-6 py-4 border-b border-slate-100 flex justify-between items-center bg-slate-50">
          <h2 className="text-lg md:text-xl font-bold flex items-center gap-2 text-slate-800"><Settings size={20} className="text-indigo-600" /> 系統設定</h2>
          <button onClick={() => setShowSettings(false)} className="p-2 hover:bg-slate-200 rounded-full text-slate-500 transition-colors"><X size={20} /></button>
        </div>
        
        <div className="flex px-2 md:px-6 border-b border-slate-200 bg-white overflow-x-auto">
          <button onClick={() => setSettingsTab('students')} className={`shrink-0 px-4 py-3 font-semibold text-sm border-b-2 transition-colors ${settingsTab === 'students' ? 'border-indigo-600 text-indigo-600' : 'border-transparent text-slate-500 hover:text-slate-700'}`}>學生名單管理</button>
          <button onClick={() => setSettingsTab('contexts')} className={`shrink-0 px-4 py-3 font-semibold text-sm border-b-2 transition-colors ${settingsTab === 'contexts' ? 'border-indigo-600 text-indigo-600' : 'border-transparent text-slate-500 hover:text-slate-700'}`}>情境/科目管理</button>
          <button onClick={() => setSettingsTab('assignments')} className={`shrink-0 px-4 py-3 font-semibold text-sm border-b-2 transition-colors ${settingsTab === 'assignments' ? 'border-indigo-600 text-indigo-600' : 'border-transparent text-slate-500 hover:text-slate-700'}`}>今日作業管理</button>
        </div>

        <div className="p-4 md:p-6 flex-1 overflow-y-auto bg-slate-50">
          {settingsTab === 'students' && (
             <div className="space-y-6">
             <div className="bg-white p-4 rounded-xl border border-slate-200 shadow-sm flex flex-wrap md:flex-nowrap items-end gap-3">
               <div className="w-[45%] md:w-20"><label className="block text-xs font-bold text-slate-500 mb-1">座號</label><input type="number" value={newStudent.no} onChange={e => setNewStudent({...newStudent, no: e.target.value})} className="w-full bg-slate-50 border border-slate-200 rounded-lg px-3 py-2 text-sm focus:ring-2 ring-indigo-500 outline-none" placeholder="1" /></div>
               <div className="w-[45%] md:w-24"><label className="block text-xs font-bold text-slate-500 mb-1">大頭貼</label><input type="text" value={newStudent.avatar} onChange={e => setNewStudent({...newStudent, avatar: e.target.value})} className="w-full bg-slate-50 border border-slate-200 rounded-lg px-3 py-2 text-sm focus:ring-2 ring-indigo-500 outline-none text-center" /></div>
               <div className="w-full md:flex-1"><label className="block text-xs font-bold text-slate-500 mb-1">姓名</label><input type="text" value={newStudent.name} onChange={e => setNewStudent({...newStudent, name: e.target.value})} className="w-full bg-slate-50 border border-slate-200 rounded-lg px-3 py-2 text-sm focus:ring-2 ring-indigo-500 outline-none" placeholder="輸入姓名" /></div>
               <button onClick={handleAddStudent} className="w-full md:w-auto bg-indigo-600 hover:bg-indigo-700 text-white px-4 py-2 rounded-lg text-sm font-bold flex justify-center items-center gap-1 transition-colors h-[38px]"><Plus size={16} /> 新增</button>
             </div>
             <div className="bg-white border border-slate-200 rounded-xl overflow-x-auto shadow-sm">
               <table className="w-full text-left text-sm min-w-[300px]">
                 <thead className="bg-slate-50 border-b border-slate-200 text-slate-500"><tr><th className="px-4 py-3 w-16 text-center">座號</th><th className="px-4 py-3">學生</th><th className="px-4 py-3 w-20 text-center">操作</th></tr></thead>
                 <tbody className="divide-y divide-slate-100">{students.map(s => (
                   <tr key={s.id} className="hover:bg-slate-50"><td className="px-4 py-3 text-center font-bold text-slate-400">{s.no}</td><td className="px-4 py-3 flex items-center gap-3"><span className="text-2xl">{s.avatar}</span><span className="font-bold text-slate-700">{s.name}</span></td><td className="px-4 py-3 text-center"><button onClick={() => handleDeleteStudent(s.id)} className="text-slate-400 hover:text-red-500 p-1.5"><Trash2 size={16} /></button></td></tr>
                 ))}</tbody>
               </table>
             </div>
           </div>
          )}

          {settingsTab === 'contexts' && (
             <div className="space-y-6">
             <div className="bg-white p-4 rounded-xl border border-slate-200 shadow-sm flex flex-col md:flex-row items-end gap-3">
               <div className="w-full md:flex-1"><label className="block text-xs font-bold text-slate-500 mb-1">新增情境 / 科目名稱</label><input type="text" value={newContext} onChange={e => setNewContext(e.target.value)} onKeyDown={e => e.key === 'Enter' && handleAddContext()} className="w-full bg-slate-50 border border-slate-200 rounded-lg px-3 py-2 text-sm focus:ring-2 ring-indigo-500 outline-none" placeholder="例如:音樂課..." /></div>
               <button onClick={handleAddContext} className="w-full md:w-auto bg-indigo-600 hover:bg-indigo-700 text-white px-4 py-2 rounded-lg text-sm font-bold flex justify-center items-center gap-1 transition-colors h-[38px]"><Plus size={16} /> 新增</button>
             </div>
             <div className="bg-white border border-slate-200 rounded-xl overflow-hidden shadow-sm">
               <ul className="divide-y divide-slate-100">{contexts.map((ctx, index) => (
                 <li key={index} className="px-5 py-3 flex justify-between items-center hover:bg-slate-50"><span className="font-bold text-slate-700 flex items-center gap-2"><BookOpen size={16} className="text-indigo-400" />{ctx}</span><button onClick={() => handleDeleteContext(ctx)} className="text-slate-400 hover:text-red-500 p-1.5"><Trash2 size={16} /></button></li>
               ))}</ul>
             </div>
           </div>
          )}

          {settingsTab === 'assignments' && (
            <div className="space-y-6">
              <div className="bg-white p-4 rounded-xl border border-slate-200 shadow-sm flex flex-col md:flex-row items-end gap-3">
                <div className="w-full md:w-1/3">
                  <label className="block text-xs font-bold text-slate-500 mb-1">關聯科目</label>
                  <select value={newAssignment.subject} onChange={e => setNewAssignment({...newAssignment, subject: e.target.value})} className="w-full bg-slate-50 border border-slate-200 rounded-lg px-3 py-2 text-sm focus:ring-2 ring-indigo-500 outline-none">
                    <optgroup label="日常作息">{routineContexts.map(ctx => <option key={ctx} value={ctx}>{ctx}</option>)}</optgroup>
                    <optgroup label="上課科目">{subjectContexts.map(ctx => <option key={ctx} value={ctx}>{ctx}</option>)}</optgroup>
                  </select>
                </div>
                <div className="w-full md:flex-1">
                  <label className="block text-xs font-bold text-slate-500 mb-1">作業項目</label>
                  <input type="text" value={newAssignment.name} onChange={e => setNewAssignment({...newAssignment, name: e.target.value})} onKeyDown={e => e.key === 'Enter' && handleAddAssignment()} className="w-full bg-slate-50 border border-slate-200 rounded-lg px-3 py-2 text-sm focus:ring-2 ring-indigo-500 outline-none" placeholder="例如:數學習作..." />
                </div>
                <button onClick={handleAddAssignment} className="w-full md:w-auto bg-indigo-600 hover:bg-indigo-700 text-white px-4 py-2 rounded-lg text-sm font-bold flex justify-center items-center gap-1 h-[38px]"><Plus size={16} /> 新增</button>
              </div>

              <div className="bg-white border border-slate-200 rounded-xl overflow-hidden shadow-sm">
                <ul className="divide-y divide-slate-100">
                  {assignments.map((assignment) => (
                    <li key={assignment.id} className="px-5 py-3 flex justify-between items-center hover:bg-slate-50">
                      <div className="flex flex-col sm:flex-row sm:items-center gap-1 sm:gap-3">
                        <span className="bg-slate-100 text-slate-600 px-2 py-1 rounded text-xs font-bold w-fit">{assignment.subject}</span>
                        <span className="font-bold text-slate-700 text-sm">{assignment.name}</span>
                      </div>
                      <button onClick={() => handleDeleteAssignment(assignment.id)} className="text-slate-400 hover:text-red-500 p-1.5"><Trash2 size={16} /></button>
                    </li>
                  ))}
                </ul>
              </div>
            </div>
          )}
        </div>
      </div>
    </div>
  );

  // ==========================================
  // VIEW: Teacher Dashboard (Responsive)
  // ==========================================
  const renderTeacherView = () => (
    <div className="flex h-screen bg-slate-50 overflow-hidden font-sans">
      {showSettings && renderSettingsModal()}
      {showMissingTable && renderMissingTableModal()}

      {/* Main Content Area */}
      <div className="flex-1 flex flex-col h-full overflow-y-auto relative scroll-smooth">
        
        {/* Responsive Header */}
        <header className="bg-white border-b border-slate-200 px-4 md:px-6 py-3 flex flex-col sm:flex-row sm:justify-between sm:items-center gap-3 sticky top-0 z-10">
          <div className="flex justify-between items-center w-full sm:w-auto">
            <div>
              <h1 className="text-xl md:text-2xl font-bold text-slate-800 flex items-center gap-2">
                <LayoutGrid className="text-indigo-600" /> The Tracker
              </h1>
              <p className="text-xs md:text-sm text-slate-500 mt-1">三年二班 - 導師主控台</p>
            </div>
            
            {/* View Mode Toggle (Mobile right, Desktop next to title) */}
            <div className="flex sm:ml-6 bg-slate-100 p-1 rounded-lg border border-slate-200">
               <button onClick={() => setTeacherViewMode('grid')} className={`flex items-center gap-1 md:gap-2 px-3 md:px-4 py-1.5 rounded-md text-xs md:text-sm font-bold transition-all ${teacherViewMode === 'grid' ? 'bg-white shadow-sm text-indigo-700' : 'text-slate-500 hover:text-slate-700'}`}>
                  <LayoutGrid size={16} /> <span className="hidden lg:inline">登記區</span>
               </button>
               <button onClick={() => setTeacherViewMode('summary')} className={`flex items-center gap-1 md:gap-2 px-3 md:px-4 py-1.5 rounded-md text-xs md:text-sm font-bold transition-all ${teacherViewMode === 'summary' ? 'bg-white shadow-sm text-indigo-700' : 'text-slate-500 hover:text-slate-700'}`}>
                  <LayoutList size={16} /> <span className="hidden lg:inline">學生總表</span>
               </button>
            </div>
          </div>
          
          <div className="flex items-center gap-2 overflow-x-auto pb-1 sm:pb-0 hide-scrollbar">
            <button onClick={() => setShowMissingTable(true)} className="flex shrink-0 items-center gap-1.5 px-3 md:px-4 py-2 bg-orange-50 hover:bg-orange-100 text-orange-700 rounded-lg font-bold transition-colors shadow-sm border border-orange-200 text-sm">
              <ClipboardList size={18} /> <span className="hidden md:inline">缺交總表</span>
              {missingRecords.length > 0 && <span className="bg-orange-500 text-white text-xs px-2 py-0.5 rounded-full ml-1">{missingRecords.length}</span>}
            </button>
            <button onClick={() => setShowSettings(true)} className="shrink-0 p-2 md:px-3 text-slate-500 hover:text-slate-700 hover:bg-slate-100 rounded-full md:rounded-lg transition-colors bg-white shadow-sm border border-slate-200 sm:border-transparent sm:shadow-none sm:bg-transparent">
              <Settings size={20} className="md:hidden" />
              <span className="hidden md:flex items-center gap-2 font-bold text-sm"><Settings size={18} /> 設定</span>
            </button>
            <button onClick={() => setView('collaborator')} className="flex shrink-0 items-center gap-1.5 px-3 md:px-4 py-2 bg-indigo-50 hover:bg-indigo-100 text-indigo-700 rounded-lg font-bold transition-colors shadow-sm text-sm">
              <Smartphone size={18} /> <span className="hidden sm:inline">科任視角</span>
            </button>
          </div>
        </header>

        {teacherViewMode === 'grid' ? (
          <>
            {/* Context Tabs (Scrollable on mobile) */}
            <div className="px-4 md:px-6 py-3 md:py-4 border-b border-slate-200 bg-white sticky top-[68px] sm:top-[76px] z-10 flex gap-2 overflow-x-auto hide-scrollbar">
              {contexts.map(ctx => (
                <button
                  key={ctx} onClick={() => setActiveContext(ctx)}
                  className={`shrink-0 px-4 py-1.5 md:py-2 rounded-full text-xs md:text-sm font-semibold transition-colors ${activeContext === ctx ? 'bg-slate-800 text-white shadow-md' : 'bg-slate-100 text-slate-600 hover:bg-slate-200'}`}
                >{ctx}</button>
              ))}
            </div>

            {/* Content Area (Grid + Sidebar stack on mobile) */}
            <div className="p-4 md:p-6 flex flex-col lg:flex-row gap-6 items-start">
              
              {/* Student Grid */}
              <div className="w-full lg:flex-1 grid grid-cols-2 sm:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 gap-3 md:gap-4">
                {students.map(student => {
                  const hasMissing = missingRecords.some(r => r.studentId === student.id);
                  return (
                    <div key={student.id} onClick={() => setSelectedStudentId(student.id)} className={`bg-white rounded-xl p-3 md:p-4 shadow-sm border-2 cursor-pointer transition-all hover:shadow-md hover:-translate-y-1 ${selectedStudentId === student.id ? 'border-indigo-500 ring-2 ring-indigo-200' : 'border-transparent'}`}>
                      <div className="flex justify-between items-start mb-2 md:mb-3">
                        <span className="text-[10px] md:text-xs font-bold text-slate-400 bg-slate-100 px-1.5 md:px-2 py-0.5 md:py-1 rounded-md">{student.no}</span>
                        <span className={`text-[10px] md:text-xs font-bold px-1.5 md:px-2 py-0.5 md:py-1 rounded-full ${student.points >= 0 ? 'bg-green-100 text-green-700' : 'bg-red-100 text-red-700'}`}>{student.points >= 0 ? '+' : ''}{student.points}</span>
                      </div>
                      <div className="flex flex-col items-center text-center">
                        <div className="text-3xl md:text-4xl mb-1 md:mb-2 select-none relative">
                          {student.avatar}
                          {hasMissing && <div className="absolute -top-1 -right-1 bg-red-500 rounded-full w-3 h-3 md:w-4 md:h-4 border-2 border-white"></div>}
                        </div>
                        <h3 className="font-bold text-slate-800 text-sm md:text-lg">{student.name}</h3>
                      </div>
                    </div>
                  );
                })}
              </div>

              {/* Master Dashboard / Right Sidebar (Stacks bottom on mobile) */}
              <div className="w-full lg:w-80 shrink-0 flex flex-col gap-6 lg:sticky lg:top-6 pb-6 lg:pb-0">
                
                {/* Missing Summary Sidebar */}
                <div className="bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden">
                  <div className="bg-red-50 px-4 py-3 border-b border-red-100 flex justify-between items-center"><div className="flex items-center gap-2"><AlertCircle className="text-red-500" size={18} /><h3 className="font-bold text-red-800 text-sm md:text-base">未交總覽快訊</h3></div></div>
                  <div className="p-4 max-h-48 lg:max-h-60 overflow-y-auto">
                    {Object.keys(missingSummaryByStudent).length > 0 ? (
                      <ul className="space-y-3">
                        {Object.entries(missingSummaryByStudent).map(([sId, records]) => {
                          const student = students.find(s => s.id === sId);
                          if (!student) return null;
                          return (
                            <li key={sId} className="text-sm border-b border-slate-100 pb-2 last:border-0 last:pb-0">
                              <div className="flex justify-between items-center"><span className="font-bold text-slate-700">{student.no}. {student.name}</span><span className="text-red-600 font-medium text-xs bg-red-50 px-2 py-1 rounded">共 {records.length} 項缺交</span></div>
                            </li>
                          );
                        })}
                      </ul>
                    ) : (
                      <p className="text-sm text-slate-500 text-center py-4">目前全班作業皆已繳齊 🎉</p>
                    )}
                  </div>
                </div>

                {/* Timeline */}
                <div className="bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden flex-1 flex flex-col max-h-[40vh] lg:max-h-[50vh]">
                  <div className="bg-slate-50 px-4 py-3 border-b border-slate-200 flex items-center gap-2"><Clock className="text-slate-500" size={18} /><h3 className="font-bold text-slate-800 text-sm md:text-base">最新動態</h3></div>
                  <div className="p-4 flex-1 overflow-y-auto">
                    <div className="relative border-l-2 border-slate-100 ml-3 space-y-6">
                      {records.map((record) => {
                        const student = students.find(s => s.id === record.studentId);
                        const isPositive = record.tagType === 'positive';
                        return (
                          <div key={record.id} className="relative pl-5 md:pl-6">
                            <div className={`absolute -left-[9px] top-1 w-4 h-4 rounded-full border-2 border-white ${record.type === 'assignment' ? 'bg-red-500' : isPositive ? 'bg-green-500' : 'bg-orange-500'}`}></div>
                            <div className="text-[10px] md:text-xs text-slate-400 mb-0.5 font-medium">{record.time} · {record.recorder} ({record.context})</div>
                            <div className="text-xs md:text-sm text-slate-700">
                              <span className="font-bold">{student ? student.name : '未知'}</span>
                              {record.type === 'assignment' ? (
                                <span> 未交 <span className="text-red-600 font-medium">{record.assignmentName}</span></span>
                              ) : (
                                <span> 標記了 <span className={`font-medium ${isPositive ? 'text-green-600' : 'text-orange-600'}`}>{record.tagName}</span></span>
                              )}
                            </div>
                          </div>
                        )
                      })}
                    </div>
                  </div>
                </div>
              </div>
            </div>
          </>
        ) : (
          /* Summary Table View */
          <div className="p-4 md:p-6">
            <div className="bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden">
              <div className="bg-slate-50 px-4 md:px-6 py-4 border-b border-slate-200 flex items-center justify-between">
                <h2 className="text-base md:text-lg font-bold text-slate-800 flex items-center gap-2">
                  <Users size={20} className="text-indigo-600" /> 全班狀況統整表
                </h2>
              </div>
              <div className="overflow-x-auto">
                <table className="w-full text-left text-sm whitespace-nowrap min-w-[700px]">
                  <thead className="bg-slate-50 border-b border-slate-200 text-slate-600 font-bold">
                    <tr>
                      <th className="px-4 md:px-6 py-4 w-16 text-center">座號</th>
                      <th className="px-4 md:px-6 py-4 w-32">學生姓名</th>
                      <th className="px-4 md:px-6 py-4 w-24 text-center">總點數</th>
                      <th className="px-4 md:px-6 py-4 w-1/3">目前缺交作業</th>
                      <th className="px-4 md:px-6 py-4">近期行為紀錄</th>
                    </tr>
                  </thead>
                  <tbody className="divide-y divide-slate-100">
                    {students.map(student => {
                      const missing = missingSummaryByStudent[student.id] || [];
                      const studentRecords = records.filter(r => r.studentId === student.id && r.type === 'tag');
                      
                      return (
                        <tr key={student.id} onClick={() => setSelectedStudentId(student.id)} className="hover:bg-indigo-50/50 transition-colors cursor-pointer group">
                          <td className="px-4 md:px-6 py-4 text-center font-bold text-slate-400 group-hover:text-indigo-500">{student.no}</td>
                          <td className="px-4 md:px-6 py-4"><div className="flex items-center gap-2"><span className="text-2xl">{student.avatar}</span><span className="font-bold text-slate-800">{student.name}</span></div></td>
                          <td className="px-4 md:px-6 py-4 text-center"><span className={`px-3 py-1.5 rounded-full font-bold text-xs ${student.points >= 0 ? 'bg-green-100 text-green-700' : 'bg-red-100 text-red-700'}`}>{student.points >= 0 ? '+' : ''}{student.points}</span></td>
                          <td className="px-4 md:px-6 py-4 whitespace-normal">
                            {missing.length > 0 ? (
                              <div className="flex flex-wrap gap-1.5">
                                {missing.map(m => (<span key={m.id} className="bg-red-50 text-red-600 border border-red-100 px-2 py-1 rounded text-[10px] md:text-xs font-bold flex items-center gap-1"><AlertCircle size={12} /> {m.name}</span>))}
                              </div>
                            ) : (<span className="text-slate-300 text-xs font-medium flex items-center gap-1"><CheckCircle2 size={14} /> 無缺交</span>)}
                          </td>
                          <td className="px-4 md:px-6 py-4 whitespace-normal">
                            {studentRecords.length > 0 ? (
                              <div className="flex flex-wrap gap-1.5">
                                {studentRecords.map(r => {
                                  const isPos = r.tagType === 'positive';
                                  return (<span key={r.id} className={`px-2 py-1 rounded text-[10px] md:text-xs font-bold border flex items-center gap-1 ${isPos ? 'bg-green-50 text-green-700 border-green-200' : 'bg-orange-50 text-orange-700 border-orange-200'}`}>{isPos ? '👍' : '⚠️'} {r.tagName}</span>);
                                })}
                              </div>
                            ) : (<span className="text-slate-300 text-xs font-medium">無紀錄</span>)}
                          </td>
                        </tr>
                      );
                    })}
                  </tbody>
                </table>
              </div>
            </div>
          </div>
        )}
      </div>

      {/* Slide-over Panel for Selected Student (Responsive Width) */}
      {selectedStudent && (
        <div className="fixed inset-0 z-50 flex justify-end">
          <div className="absolute inset-0 bg-slate-900/40 backdrop-blur-sm transition-opacity" onClick={() => setSelectedStudentId(null)}></div>
          
          <div className="relative w-[85vw] sm:w-[400px] bg-white shadow-2xl h-full flex flex-col transform transition-transform duration-300 border-l border-slate-200">
            <div className="px-4 md:px-6 py-4 md:py-5 border-b border-slate-100 flex items-center justify-between bg-slate-50">
              <div className="flex items-center gap-3 md:gap-4">
                <div className="text-3xl md:text-4xl">{selectedStudent.avatar}</div>
                <div>
                  <h2 className="text-lg md:text-xl font-bold text-slate-800">{selectedStudent.no}. {selectedStudent.name}</h2>
                  <div className={`text-xs md:text-sm font-semibold mt-0.5 md:mt-1 ${selectedStudent.points >= 0 ? 'text-green-600' : 'text-red-600'}`}>
                    總點數: {selectedStudent.points}
                  </div>
                </div>
              </div>
              <button onClick={() => setSelectedStudentId(null)} className="p-2 text-slate-400 hover:text-slate-600 hover:bg-slate-200 rounded-full transition-colors"><X size={20} /></button>
            </div>

            <div className="p-4 md:p-6 overflow-y-auto flex-1 flex flex-col gap-6 md:gap-8 pb-10">
              
              <section>
                <h3 className="text-xs md:text-sm font-bold text-slate-400 uppercase tracking-wider mb-3 md:mb-4 flex items-center gap-2">
                  <ListChecks size={16} /> 今日預設作業 
                </h3>
                <div className="space-y-2 md:space-y-3">
                  {assignments.map(assignment => {
                    const isMissing = isMissingToday(selectedStudent.id, assignment.id);
                    return (
                      <label key={assignment.id} className={`flex items-center justify-between p-2.5 md:p-3 rounded-lg border-2 cursor-pointer transition-all ${isMissing ? 'border-red-200 bg-red-50/50' : 'border-slate-100 hover:border-indigo-200 bg-white'}`}>
                        <div><span className="block text-[9px] md:text-[10px] text-slate-400 font-bold mb-0.5">{assignment.subject}</span><span className={`text-sm md:text-base font-medium ${isMissing ? 'text-red-600 line-through opacity-70' : 'text-slate-700'}`}>{assignment.name}</span></div>
                        <div className={`w-5 h-5 md:w-6 md:h-6 rounded flex items-center justify-center transition-colors ${!isMissing ? 'bg-indigo-600 text-white' : 'bg-slate-200 text-transparent'}`}><Check size={14} strokeWidth={3} /></div>
                        <input type="checkbox" className="hidden" checked={!isMissing} onChange={() => toggleTodayAssignment(selectedStudent.id, assignment)} />
                      </label>
                    );
                  })}
                </div>
              </section>

              <section className="bg-red-50/50 rounded-xl border border-red-100 p-3 md:p-4">
                <h3 className="text-xs md:text-sm font-bold text-red-800 uppercase tracking-wider mb-3 md:mb-4 flex items-center gap-2">
                  <Calendar size={16} /> 缺交作業清單與補登
                </h3>
                <div className="space-y-2 mb-3 md:mb-4">
                  {(missingSummaryByStudent[selectedStudent.id] || []).map(record => (
                     <div key={record.id} className="flex flex-col sm:flex-row sm:items-center justify-between bg-white p-2.5 rounded-lg border border-red-200 shadow-sm gap-2">
                       <div className="flex flex-col"><div className="flex items-center gap-2"><span className="text-[9px] md:text-[10px] font-bold bg-slate-100 text-slate-500 px-1.5 py-0.5 rounded">{record.date}</span><span className="text-[9px] md:text-[10px] font-bold text-indigo-500">{record.subject}</span></div><span className="font-bold text-red-600 text-xs md:text-sm mt-0.5">{record.name}</span></div>
                       <button onClick={() => markMissingAsDone(record.id)} className="w-full sm:w-auto text-center bg-green-100 text-green-700 hover:bg-green-500 hover:text-white px-2 py-1.5 md:py-1 rounded text-xs font-bold transition-colors border border-green-200">已完成</button>
                     </div>
                  ))}
                  {!(missingSummaryByStudent[selectedStudent.id] || []).length && (<div className="text-xs text-slate-400 text-center py-2">目前沒有任何缺交紀錄</div>)}
                </div>
                <div className="border-t border-red-100 pt-3 mt-2">
                  <p className="text-[10px] md:text-xs font-bold text-slate-500 mb-2">➕ 手動補登歷史缺交</p>
                  <div className="flex flex-col gap-2">
                    <div className="grid grid-cols-2 gap-2">
                       <input type="date" value={manualMissing.date} onChange={e => setManualMissing({...manualMissing, date: e.target.value})} className="bg-white border border-slate-200 rounded px-2 py-1.5 text-xs focus:ring-1 ring-indigo-500 outline-none w-full" />
                       <select value={manualMissing.subject} onChange={e => setManualMissing({...manualMissing, subject: e.target.value})} className="bg-white border border-slate-200 rounded px-2 py-1.5 text-xs focus:ring-1 ring-indigo-500 outline-none w-full">
                          <optgroup label="日常作息">{routineContexts.map(ctx => <option key={ctx} value={ctx}>{ctx}</option>)}</optgroup>
                          <optgroup label="上課科目">{subjectContexts.map(ctx => <option key={ctx} value={ctx}>{ctx}</option>)}</optgroup>
                       </select>
                    </div>
                    <div className="flex gap-2">
                       <input type="text" placeholder="輸入作業名稱" value={manualMissing.name} onChange={e => setManualMissing({...manualMissing, name: e.target.value})} className="flex-1 bg-white border border-slate-200 rounded px-2 py-1.5 text-xs focus:ring-1 ring-indigo-500 outline-none" />
                       <button onClick={() => handleAddManualMissing(selectedStudent.id)} className="bg-red-600 hover:bg-red-700 text-white px-3 py-1.5 rounded text-xs font-bold transition-colors whitespace-nowrap">補登</button>
                    </div>
                  </div>
                </div>
              </section>

              <section>
                <div className="flex justify-between items-center mb-3 md:mb-4">
                  <h3 className="text-xs md:text-sm font-bold text-slate-400 uppercase tracking-wider flex items-center gap-2">
                    <Award size={16} /> 行為表現
                  </h3>
                  <select 
                    value={studentPanelContext} 
                    onChange={(e) => setStudentPanelContext(e.target.value)}
                    className="bg-indigo-50 border border-indigo-200 text-indigo-700 text-[10px] md:text-xs font-bold rounded-lg px-2 py-1 outline-none cursor-pointer max-w-[120px] md:max-w-none"
                  >
                    <optgroup label="日常作息">{routineContexts.map(c => <option key={c} value={c}>{c}</option>)}</optgroup>
                    <optgroup label="上課科目">{subjectContexts.map(c => <option key={c} value={c}>{c}</option>)}</optgroup>
                  </select>
                </div>
                
                <div className="grid grid-cols-2 gap-2 md:gap-3">
                  {contextTags.map(tag => (
                    <button 
                      key={tag.id} 
                      onClick={() => addRecord(selectedStudent.id, 'tag', tag, false, studentPanelContext)} 
                      className={`p-2.5 md:p-3 rounded-xl border-2 text-xs md:text-sm font-bold transition-all hover:shadow-md active:scale-95 flex flex-col items-center gap-1 ${tag.type === 'positive' ? 'border-green-100 bg-green-50 text-green-700 hover:border-green-300' : 'border-orange-100 bg-orange-50 text-orange-700 hover:border-orange-300'}`}
                    >
                      <span className="text-base md:text-lg">{tag.type === 'positive' ? '👍' : '⚠️'}</span>
                      {tag.name}
                    </button>
                  ))}
                  
                  <button onClick={() => handleCustomTag('positive', false)} className="p-2.5 md:p-3 rounded-xl border-2 border-dashed border-green-200 bg-slate-50 text-xs md:text-sm font-bold transition-all hover:bg-green-100 text-green-600 flex flex-col items-center gap-1">
                    <span className="text-base md:text-lg">👍</span> 其他(自訂)
                  </button>
                  <button onClick={() => handleCustomTag('needs_improvement', false)} className="p-2.5 md:p-3 rounded-xl border-2 border-dashed border-orange-200 bg-slate-50 text-xs md:text-sm font-bold transition-all hover:bg-orange-100 text-orange-600 flex flex-col items-center gap-1">
                    <span className="text-base md:text-lg">⚠️</span> 其他(自訂)
                  </button>
                </div>
              </section>
            </div>
          </div>
        </div>
      )}
    </div>
  );

  // ==========================================
  // VIEW: Collaborator Portal (Already Mobile-First)
  // ==========================================
  const renderCollaboratorView = () => (
    <div className="min-h-screen bg-slate-100 flex justify-center font-sans">
      <div className="w-full max-w-md bg-white min-h-screen shadow-2xl relative flex flex-col pb-24">
        <div className="bg-indigo-600 text-white px-5 py-6 rounded-b-3xl shadow-lg relative z-10">
          <div className="flex justify-between items-start mb-4">
            <h1 className="text-xl font-bold">The Tracker</h1>
            <button onClick={() => setView('teacher')} className="text-xs bg-white/20 hover:bg-white/30 px-3 py-1.5 rounded-full backdrop-blur-sm transition-colors font-bold tracking-wider">返回導師</button>
          </div>
          <div className="bg-white/10 rounded-xl p-3 backdrop-blur-md border border-white/20">
            <p className="text-xs text-indigo-100 mb-2">目前正在紀錄:三年二班</p>
            <div className="flex items-center bg-white rounded-lg p-1">
               <BookOpen size={16} className="text-indigo-600 ml-2" />
               <select value={collabContext} onChange={(e) => setCollabContext(e.target.value)} className="w-full bg-transparent text-indigo-900 font-bold text-sm px-2 py-1.5 outline-none cursor-pointer">
                 <optgroup label="上課科目">{subjectContexts.map(ctx => <option key={ctx} value={ctx}>{ctx}</option>)}</optgroup>
                 <optgroup label="日常作息">{routineContexts.map(ctx => <option key={ctx} value={ctx}>{ctx}</option>)}</optgroup>
               </select>
            </div>
          </div>
        </div>

        <div className="p-5 flex-1 overflow-y-auto">
          <div className="flex justify-between items-center mb-4">
            <p className="text-sm text-slate-500 font-bold flex items-center gap-1"><Check size={16} className="text-indigo-500" /> 點選學生批次給分</p>
            {collabSelectedStudents.length > 0 && <span className="text-xs font-bold text-indigo-600 bg-indigo-50 px-2 py-1 rounded-full">已選 {collabSelectedStudents.length} 人</span>}
          </div>
          <div className="grid grid-cols-3 gap-3">
            {students.map(student => {
              const isSelected = collabSelectedStudents.includes(student.id);
              return (
                <div key={student.id} onClick={() => setCollabSelectedStudents(prev => isSelected ? prev.filter(id => id !== student.id) : [...prev, student.id])} className={`relative flex flex-col items-center p-3 rounded-2xl border-2 cursor-pointer transition-all duration-200 ${isSelected ? 'border-indigo-600 bg-indigo-50 shadow-md transform scale-105' : 'border-slate-100 bg-white shadow-sm'}`}>
                  {isSelected && <div className="absolute -top-2 -right-2 bg-indigo-600 text-white rounded-full p-0.5 shadow-sm"><Check size={14} strokeWidth={3} /></div>}
                  <div className="text-3xl mb-1 select-none">{student.avatar}</div>
                  <span className={`text-sm font-bold ${isSelected ? 'text-indigo-800' : 'text-slate-700'}`}>{student.name}</span>
                  <span className="text-[10px] text-slate-400 font-bold mt-0.5">{student.no}號</span>
                </div>
              );
            })}
          </div>
        </div>

        <div className={`fixed bottom-0 left-0 right-0 md:absolute bg-white border-t border-slate-200 shadow-[0_-10px_40px_-10px_rgba(0,0,0,0.1)] px-5 py-4 pb-8 md:pb-4 transition-transform duration-300 ease-in-out z-20 ${collabSelectedStudents.length > 0 ? 'translate-y-0' : 'translate-y-full'}`}>
          <div className="flex justify-between items-center mb-3">
            <span className="font-bold text-indigo-800">選擇行為標籤:</span>
            <button onClick={() => setCollabSelectedStudents([])} className="text-xs bg-slate-100 text-slate-500 px-2 py-1 rounded-md hover:bg-slate-200 font-bold">取消全選</button>
          </div>
          <div className="flex gap-2 overflow-x-auto pb-2 -mx-2 px-2 snap-x hide-scrollbar">
            {collabTags.map(tag => (
              <button key={tag.id} onClick={() => handleCollabSubmit(tag)} className={`snap-start shrink-0 px-4 py-3 rounded-xl font-bold text-sm transition-transform active:scale-95 flex items-center gap-2 ${tag.type === 'positive' ? 'bg-green-100 text-green-700 border border-green-200 hover:bg-green-200' : 'bg-orange-100 text-orange-700 border border-orange-200 hover:bg-orange-200'}`}>
                {tag.type === 'positive' ? '👍' : '⚠️'}{tag.name}
              </button>
            ))}
            <button onClick={() => handleCustomTag('positive', true)} className="snap-start shrink-0 px-4 py-3 rounded-xl font-bold text-sm transition-transform active:scale-95 flex items-center gap-2 bg-slate-50 text-green-700 border-2 border-dashed border-green-300 hover:bg-green-100">👍 其他(自訂)</button>
            <button onClick={() => handleCustomTag('needs_improvement', true)} className="snap-start shrink-0 px-4 py-3 rounded-xl font-bold text-sm transition-transform active:scale-95 flex items-center gap-2 bg-slate-50 text-orange-700 border-2 border-dashed border-orange-300 hover:bg-orange-100">⚠️ 其他(自訂)</button>
          </div>
        </div>

        {showToast && (
          <div className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 bg-slate-800 text-white px-6 py-4 rounded-2xl shadow-2xl flex flex-col items-center gap-2 z-50 animate-bounce">
            <div className="bg-green-500 rounded-full p-2"><Check size={24} strokeWidth={3} className="text-white" /></div>
            <span className="font-bold text-lg">紀錄已送出!</span>
            <span className="text-xs text-slate-300">已同步至導師主控台</span>
          </div>
        )}
      </div>
    </div>
  );

  return view === 'teacher' ? renderTeacherView() : renderCollaboratorView();
}
💡 給讀者的小撇步: 如果你想讓紀錄「長久保存」,可以在請 Gemini 修改時多加一句指令:「請幫我在代碼中加入 localStorage 功能,讓我重新整理網頁後紀錄也能保留。」這樣功能就更完美囉!

 
 
 

留言


© 2023 by Train of Thoughts. Proudly created with Wix.com

bottom of page