3hr 研習課程設計|Google Workspace × Apps Script

一週三報|大學課程週報工作流與 GAS 實作

從每週課程內容重點出發,產出學生學習報、課程紀錄報、成果亮點報,讓課程進度能被學生看見,讓教學歷程能被教師留存,讓課程成果能被外界理解。

研習時間3 小時
對象大學教授、講師、兼任教師、課程協作教師
工具Google Sheets、Apps Script、Docs、Drive
成果完成一套課程週報 MVP
大學課程週報管理系統一週三報自動化整合方案流程圖
大學課程版:課程資料、GAS 整理與一週三報分流

一、研習定位與總目標

這不是單純教一支 GAS,而是讓教師建立可重複使用的課程資訊工作流。

大學課程每週都會產生進度、指定閱讀、作業提醒、小組討論、實作照片、學生作品、助教協作與成果素材。如果沒有固定流程,這些資訊會散落在 LMS、LINE、Email、簡報、雲端硬碟與教師個人筆記中。

每週整理一次,產出三種用途;讓課程進度能被學生看見,讓教學歷程能被教師留存,讓課程成果能被外界理解。

研習結束後,學員能完成

理解工作流

理解課程資訊流、形成性回饋、教學歷程留存、分眾溝通與成果導向。

實作 GAS MVP

建立 Google Sheet 中控台、產生一週三報、Docs、資料夾與歷週索引。

轉化到自己的課

能把模板改造成實作課、實習課、專題課、服務學習或教學實踐研究課程。

三報定義

為什麼是三報?

三報不是三份重複工作,而是同一份課程資料的三種轉譯。學生學習報解決學生不知道本週重點與下週任務的問題;課程紀錄報解決教師、助教與系辦期末才回頭補紀錄的問題;成果亮點報解決外部只看到照片卻看不懂課程價值的問題。

報別主要對象使用情境文案特性
學生學習報修課學生LMS、Google Classroom、Moodle、課程公告、學生群組清楚、任務導向、提醒下一步
課程紀錄報教師、助教、系辦、課程團隊課程留存、助教協作、教學檢討、教學實踐研究正式、可追蹤、可回顧
成果亮點報系所、粉專、招生、成果展示系所粉專、招生素材、成果報告、課程亮點紀錄敘事、展示、對外溝通

二、學理基礎

讓教授講師知道這套工具背後是教學設計與資訊管理,而不是單純自動化。

1. 課程經營也是資訊流管理

概念說明:課程不是只有上課本身,而是每一週都會產生許多需要被整理、提醒、留存與轉述的資訊。例如本週課程重點、指定閱讀、作業提醒、課堂活動、學生表現、實作照片、助教協作事項、下週準備事項與成果素材。

課程現場問題:如果沒有固定流程,這些資訊很容易散落在 LMS、LINE、Email、簡報、雲端硬碟、教師個人筆記與手機相簿中。等到期中、期末或成果報告需要整理時,教師與助教才回頭翻找,往往會耗費更多時間,也容易遺漏教學現場的重要脈絡。

一週三報如何回應:一週三報的核心不是要求教師「多寫三份報告」,而是先把同一份課程原始資料整理成穩定欄位,再依照不同用途轉換成三種資訊產品:給學生看的學習報、給教學團隊看的紀錄報、給外部受眾看的成果亮點報。

課堂原始資料
週期化整理
一週三報
發布與留存
期末成果累積

2. 形成性回饋與學生學習報

概念說明:形成性回饋的重點,是在學習過程中讓學生知道自己正在往哪裡走,而不是等到期中、期末才用一次大型評量回頭檢查。學生學習報對應的是學習節奏管理,協助學生看見這週學了什麼、下週要準備什麼,以及作業、閱讀、專題或實作任務接下來該做哪一步。

課程現場問題:對大學課程來說,學生常常同時修多門課,資訊若只停留在口頭提醒或散落公告中,很容易被忽略。學生可能到期中期末才發現自己缺作業、沒讀指定材料,或不清楚專題進度應該推到哪裡。

一週三報如何回應:每週固定一次的學生學習報,可以把課程重點、任務提醒與下一步行動集中呈現,讓學生在學習過程中持續校準方向,而不是只在大型評量後才收到回饋。

學生學習報不是額外考核,而是讓學生每週看見學習節奏、任務節點與下一步行動。

3. 教學歷程留存與證據導向教學

概念說明:課程紀錄報的主要功能,是把每週教學歷程留下來。它可以記錄課程進度、學生學習觀察、課堂活動、照片與附件索引、教學調整依據、助教協作事項與系辦需要知道的流程資訊。

課程現場問題:許多課程的成果其實每週都在發生,但期末才需要寫報告、接受評鑑、整理教學實踐研究素材或交付系所成果時,才發現照片、作品、助教紀錄與教學調整理由分散在不同地方。

一週三報如何回應:課程紀錄報把每週教學活動、協作事項與可引用證據固定留存,支援教學檢討、助教協作、期末成果、評鑑、課程認證與教學實踐研究。對共同授課、實作課、場域課程或有助教協作的課程而言,固定紀錄也能降低資訊只存在某一位教師或助教腦中的風險。

不要等到期末才回頭補寫成果,而是在每一週自然留下可整理、可追蹤、可引用的教學證據。

4. 分眾溝通與媒介適配

概念說明:同一份課程資訊不能原封不動貼到所有平台,因為不同對象需要的語氣、資訊密度與行動提示不同。分眾溝通的重點不是把訊息切碎,而是讓對的人收到剛好能採取行動的資訊。

課程現場問題:學生需要任務導向、清楚提醒與下一步行動;教師、助教與系辦需要正式紀錄、流程追蹤與協作事項;外部受眾、系所粉專或招生受眾,則需要能看懂課程價值的成果敘事。如果三種對象都看到同一段文字,通常會同時出現太長、太短、太內部或不夠具體的問題。

一週三報如何回應:一週三報不是重複勞動,而是「一份資料,多種轉譯」。同一份課程原始資料,經過不同語氣與資訊密度的調整,就能同時服務學習管理、內部協作與對外溝通。

5. 成果導向與課程品牌經營

概念說明:成果亮點報不是單純宣傳,而是讓外部人員理解課程價值。課程品牌經營不是把課包裝得華麗,而是穩定說明課程如何設計、學生如何參與、最後展現哪些能力。

課程現場問題:尤其實作課、實習課、專題課、服務學習與場域課程,若只是張貼照片,外部的人不一定看得懂學生做了什麼、學到了什麼、展現了什麼能力。照片本身是素材,還需要簡短敘事把活動、學習與能力連起來。

一週三報如何回應:成果亮點報把課堂活動、學生作品、場域互動與能力展現連接起來,讓系所粉專、成果網站、招生素材或期末成果彙整不只是「有活動」,而是能說明課程設計與學生學習成果。

6. 為什麼選擇 LINE@、FB 粉絲團與 Google 協作平台

一週三報選擇平台時,不是只看平台功能多不多,而是看受眾在哪裡、資訊是否需要即時觸達、資訊是否需要長期留存、資訊是否需要對外展示,以及日後是否容易被再次找到。LINE@、FB 粉絲團與 Google 協作平台只是常見組合,實際上可以依學校、系所、課程需求改成其他發布組合。

LINE@/LINE 官方帳號

定位是即時觸達、提醒與推播,適合放本週提醒、下週任務、課程公告、活動提醒、繳交期限,以及給學生或家長的短訊息。LINE 的觸達率高、即時性強,但不適合長期保存與系統化檢索,因此適合當作提醒入口,不適合作為唯一的課程知識庫。

FB 粉絲團

定位是成果曝光、社群擴散與課程品牌,適合放課程亮點、學生作品、活動照片、系所成果、招生素材與對外敘事。FB 粉絲團能讓外部受眾理解課程特色,但不適合承載完整課程資料與內部協作紀錄,因此適合做成果展示櫥窗。

Google 協作平台

定位是長期留存、結構化整理與可回顧的課程網站,適合放歷週課程索引、課程週報彙整、課程成果頁、學生作品索引、教學歷程資料與期末成果整理。它能讓課程資料從一次性貼文,變成可長期累積的課程網站。

平台 主要功能 適合放什麼 不適合做什麼 在一週三報中的角色
LINE@/LINE 官方帳號 即時觸達與提醒 本週提醒、下週任務、繳交期限、課程公告 長期保存、系統化檢索、完整課程資料庫 提醒入口
FB 粉絲團 對外曝光與社群擴散 課程亮點、活動照片、學生作品、成果敘事、招生素材 內部紀錄、完整教材、正式協作資料 成果櫥窗
Google 協作平台 長期留存與結構化整理 歷週課程索引、課程成果頁、學生作品索引、教學歷程資料 即時推播、短訊息提醒 課程網站與成果資料庫
LMS/Google Classroom/Moodle 修課學生學習管理 作業、教材、評量、課程公告 對外展示、招生宣傳、公開成果累積 學習任務平台
Google Docs/Drive 文件與附件保存 正式紀錄、照片、附件、報告、原始資料 對外瀏覽體驗、社群擴散 後台資料庫
一週三報不是要求教師每週到處發文,而是先建立一份標準化課程資料,再依照不同平台的功能轉譯成不同版本:LINE 負責提醒,FB 負責曝光,Google 協作平台負責長期留存,LMS 負責學習任務,Drive 負責原始資料保存。

7. 全流程概念圖

講師可用這個流程帶學員理解:工具只是中間環節,真正要建立的是「每週原始資料如何被整理、轉譯、發布、留存」的課程工作流。

每週課程原始資料
Google Sheet 中控台
GAS 自動整理
學生學習報|課程紀錄報|成果亮點報
LINE/LMS|Docs/Drive|FB/Google 協作平台
歷週課程索引
期末成果、教學實踐、系所成果資料庫

三、3 小時研習流程

先建立概念,再展示成果,接著實作,最後導向擴充與轉化。

時間主題講師任務學員任務產出
00:00–00:15開場與痛點引導引導討論課程資訊為什麼會散落在 LMS、群組、Email、雲端硬碟、照片與個人筆記。標出自己課程每週資訊會出現在哪些地方。學員能說出自己課程資訊散落在哪裡。
00:15–00:40學理:一週三報工作流說明資訊流、形成性回饋、歷程留存、分眾溝通、成果導向與平台分流。把自己的課程資訊初步分類到學生、內部協作、對外成果三種用途。學員理解一週三報的學理基礎。
00:40–01:00成品展示示範從一筆課程資料產出三報、Google Docs、課程平台素材與歷週索引。觀察成品輸入欄位、輸出文案與發布紀錄之間的關係。學員知道完成品長什麼樣子。
01:00–01:25GAS 架構拆解拆解 Sheet 是資料庫、GAS 是流程引擎、Docs 是正式文件、Sidebar 是前端操作。對照自己的畫面,確認每張工作表的角色。學員能說明中控台、資料表、輸出表與發布紀錄的用途。
01:25–01:35休息協助處理登入、授權、瀏覽器或帳號問題。完成登入與環境確認。降低下半場實作卡關風險。
01:35–02:05GAS 實作一:建立平台帶學員建立 Google Sheet、貼上 GAS、授權、初始化平台。依步驟完成初始化,確認產生 8 張工作表,包含 07_學生名單學員完成 GAS 初始化。
02:05–02:35GAS 實作二:填資料與產三報帶學員設定課程資料、建立本週課程週報、填入課程內容、產出三報與 Google Docs;進度較快者可登錄一筆測試學生資料。完成一筆週報資料,執行產生三報。學員完成一筆週報資料與三報產出。
02:35–02:50前端操作與發布流程示範開啟本週課程發布面板,複製三報,標記發布,更新歷週索引;若需測試 Email,示範寄送前確認與 BCC 寄送邏輯。開啟發布面板,複製至少一則文案,標記一次發布紀錄。學員能開啟發布面板並複製三報。
02:50–03:00後續擴充與轉化提示 Gemini、Google Chat、期末成果彙整、不同課程模板與系所成果資料庫。寫下自己的課程可先擴充哪一個部分。學員寫下一個後續擴充構想。

四、示範資料與互動任務

提供貼近民生學院的課程情境,避免學員卡在不知道要填什麼。

展示情境:餐旅服務管理

課程名稱:餐旅服務管理
授課教師:王老師
系所/單位:民生學院餐旅管理系
週次:2026-W27
日期區間:2026/06/29–2026/07/03

本週課程重點:
- 服務接待流程
- 顧客抱怨處理
- 情緒安撫與問題解決

本週課堂活動/實作內容:
- 小組進行情境演練
- 角色分工:服務人員、顧客、觀察員
- 討論不同回應策略對顧客感受的影響

學生學習表現/討論亮點:
- 第二組能兼顧情緒安撫與問題解決
- 多數學生開始能使用專業語彙描述服務流程
- 部分學生仍需加強現場判斷與回應彈性

下週課前準備與任務:
- 繳交服務流程觀察紀錄
- 完成小組期中專題提案初稿
- 準備一個服務失誤案例於課堂討論

助教/系辦/業師/合作單位協作事項:
- 助教協助彙整各組提案主題
- 課程照片上傳至本週資料夾
- 下週確認期中提案報告順序

三組示範課程情境

示範資料不一定要使用餐旅案例。講師可依學員背景,選擇最接近的課程情境,讓學員更快把欄位轉換成自己的課程語言。

餐旅服務管理

聚焦服務流程、顧客溝通與情境演練,適合示範如何把課堂活動、學生表現與對外成果敘事連在一起。

幼兒活動設計

聚焦活動設計、教案實作、幼兒觀察與試教反思,適合示範如何記錄教案發展、試教修正與學習觀察。

高齡服務方案設計

聚焦需求評估、場域服務、方案設計與服務反思,適合示範場域課程如何留下協作紀錄與成果素材。

三個互動任務

任務一:找出資訊流

請學員寫下自己課程中每週會出現的資訊:課程重點、活動、學生表現、下週提醒、協作事項、需要留存的成果。

任務二:分類到三報

將「下週三交期中提案」「第二組討論表現佳」「助教整理照片」分類到學生學習報、課程紀錄報、成果亮點報或協作通知。

任務三:改寫成果亮點

把「今天做小組服務情境演練,學生表現不錯」改寫成可放在系所粉專或成果報告的課程亮點敘事。

成果亮點改寫範例

本週課程中,學生透過服務情境演練,練習面對真實服務現場中的溝通挑戰。從接待、傾聽到提出解決方案,學生逐步理解專業服務不只是流程,更是對人的理解。

取得 GAS 程式碼

高教課程版使用專案內的 一週三報_大學課程內容重點.gs,可直接從本頁複製。此版本已包含學生首次登錄、07_學生名單 與週三報 Email 寄送功能。

使用方式:請先建立 Google Sheet,接著開啟「擴充功能 → Apps Script」,將下方 GAS 程式碼完整複製貼上後儲存,再回到試算表重新整理。
適用版本:高教課程版 適用對象:大學課程、專題課、實作課、場域課程 程式來源:一週三報_大學課程內容重點.gs 版本:依專案目前檔案內容 本次更新:學生首次登錄、07_學生名單、週三報 Email 寄送
展開完整 GAS 程式碼
/**
 * 一週三報|大學課程內容重點工作流平台
 *
 * 版本:University MVP 0.2
 * 使用方式:
 * 1. 建立新的 Google Sheet
 * 2. 擴充功能 → Apps Script
 * 3. 貼上本檔為 Code.gs
 * 4. 儲存並重新整理試算表
 * 5. 使用「一週三報|大學課程內容重點」選單操作
 *
 * 本檔是獨立版本,不會修改既有「一週三報.gs」。
 * 自訂函式使用 UCR_ 前綴,降低與其他 GAS 檔案命名衝突的機率。
 */

const UCR_CONFIG = {
  TIMEZONE: "Asia/Taipei",
  PLATFORM_NAME: "一週三報|大學課程內容重點工作流平台",

  SHEETS: {
    DASHBOARD: "00_課程中控台",
    WEEKLY_DATA: "01_課程週次資料",
    COPY_OUTPUT: "02_三端內容輸出",
    PUBLISH_LOG: "03_發布紀錄",
    SETTINGS: "04_設定",
    INDEX: "05_歷週索引",
    PLATFORM_MATERIALS: "06_課程平台發布素材",
    STUDENTS: "07_學生名單",
  },

  DEFAULT_SETTINGS: [
    ["設定項目", "設定值", "說明"],
    ["課程名稱", "大學課程名稱", "顯示於標題、Docs 與發布素材"],
    ["授課教師", "授課教師姓名", "顯示於署名與通知"],
    ["開課系所", "開課系所", "顯示於課程資訊"],
    ["學期", "114-1", "例如 114-1、2026 Spring"],
    ["課程平台網址", "", "LMS、Moodle、Google Classroom 或課程網站網址"],
    ["課程協作空間網址", "", "Notion、Google Chat 空間說明頁、共編文件等,可空白"],
    ["Drive根資料夾ID", "", "可空白;空白時會自動建立根資料夾"],
    ["Google Chat Webhook URL", "", "可空白;用於發布課程內容重點摘要到協作空間"],
    ["是否啟用協作平台發布", "否", "填「是」才允許一鍵發布到協作平台"],
    ["協作平台名稱", "Google Chat", "顯示於中控台與發布紀錄"],
  ],

  PLACEHOLDERS: {
    topic: "本週課程單元或主題:",
    concepts: "核心概念/理論重點:\n- ",
    activities: "課堂活動、案例、討論或實作:\n- ",
    assignments: "指定閱讀、作業、測驗或繳交事項:\n- ",
    nextPreview: "下週預告與預習方向:\n- ",
    resources: "補充資源、影片、資料集或延伸閱讀連結:",
  },

  STATUS: {
    NOT_CREATED: "⚪ 尚未建立",
    DRAFT: "🟡 草稿中",
    COPY_READY: "🟢 已產生三端內容",
    DOC_READY: "🔵 已產生 Docs",
    COLLAB_READY: "🟣 已發布協作平台",
    COMPLETED: "✅ 已完成發布",
  },
};

function onOpen() {
  UCR_onOpen();
}

function UCR_onOpen() {
  SpreadsheetApp.getUi()
    .createMenu("一週三報|大學課程內容重點")
    .addItem("1. 初始化平台", "UCR_initPlatform")
    .addItem("重新整理中控台", "UCR_refreshDashboardView")
    .addSeparator()
    .addItem("2. 建立本週課程重點範本", "UCR_createCurrentWeekReport")
    .addItem("建立下一週課程重點範本", "UCR_createNextWeekReport")
    .addItem("開啟課程週次填寫表單", "UCR_showWeeklyInputForm")
    .addItem("3. 建立本週課程素材資料夾", "UCR_createMaterialsFolderForActiveWeek")
    .addSeparator()
    .addItem("開啟學生首次登錄表單", "UCR_showStudentRegisterForm")
    .addItem("開啟學生名單", "UCR_openStudentsSheet")
    .addSeparator()
    .addItem("開啟本週發布面板", "UCR_showWeeklyPublishPanel")
    .addSeparator()
    .addItem("4. 產生三端內容", "UCR_generateThreeChannelCopies")
    .addItem("5. 產生 Google Docs 課程重點", "UCR_generateGoogleDocForActiveWeek")
    .addItem("產生課程平台發布素材", "UCR_generatePlatformPublishMaterials")
    .addItem("一鍵發布到協作平台", "UCR_publishToCollaborationPlatform")
    .addSeparator()
    .addItem("標記課程平台已發布", "UCR_markCoursePlatformPublished")
    .addItem("寄送本週週三報給學生", "UCR_sendWeeklyEmailToStudents")
    .addItem("標記 Email 已發布", "UCR_markEmailPublished")
    .addItem("標記 LMS/社群已發布", "UCR_markLmsPublished")
    .addSeparator()
    .addItem("更新歷週索引", "UCR_updateWeeklyIndex")
    .addToUi();
}

function UCR_initPlatform() {
  UCR_runAction_("初始化平台", function () {
    const ss = SpreadsheetApp.getActiveSpreadsheet();
    UCR_setupDashboardSheet_(ss);
    UCR_setupWeeklyDataSheet_(ss);
    UCR_setupCopyOutputSheet_(ss);
    UCR_setupPublishLogSheet_(ss);
    UCR_setupSettingsSheet_(ss);
    UCR_setupIndexSheet_(ss);
    UCR_setupPlatformMaterialsSheet_(ss);
    UCR_ensureStudentsSheet_(ss);
    UCR_alert_("初始化完成。請先到「04_設定」確認課程資料,再建立本週課程重點範本。");
  });
}

function UCR_refreshDashboardView() {
  UCR_runAction_("重新整理中控台", function () {
    const ss = SpreadsheetApp.getActiveSpreadsheet();
    const activeWeekId = UCR_getActiveWeekId_() || UCR_getLatestWeekId_();
    UCR_setupDashboardSheet_(ss);
    UCR_setupIndexSheet_(ss);
    UCR_ensurePlatformMaterialsSheet_(ss);
    UCR_refreshStudentsSummaryOnDashboard_();
    if (activeWeekId) {
      UCR_refreshDashboardForWeek_(activeWeekId);
      UCR_updateWeeklyIndex_();
    }
    UCR_alert_("中控台已重新整理,不會清空課程週次資料與三端內容。");
  });
}

function UCR_setupDashboardSheet_(ss) {
  const sheet = UCR_getOrCreateSheet_(ss, UCR_CONFIG.SHEETS.DASHBOARD);
  sheet.clear();

  const values = [
    [UCR_CONFIG.PLATFORM_NAME, "", "", "", "", ""],
    ["操作提示", "建立課程週次 → 填寫課程重點 → 產生三端內容 → 產生 Docs / 平台素材 → 發布與標記", "", "", "", ""],
    ["", "", "", "", "", ""],
    ["目前週次ID", "", "", "總狀態", UCR_CONFIG.STATUS.NOT_CREATED, ""],
    ["日期區間", "", "", "Google Docs", "尚未建立", ""],
    ["課程平台", "尚未設定", "", "課程素材資料夾", "尚未建立", ""],
    ["", "", "", "", "", ""],
    ["流程狀態", "", "", "", "", ""],
    ["課程草稿", "⚪ 尚未建立", "三端內容", "⚪ 尚未產生", "Google Docs", "⚪ 尚未產生"],
    ["外部發布", "⚪ 尚未完成", "協作平台", "⚪ 尚未發布", "", ""],
    ["", "", "", "", "", ""],
    ["發布狀態", "", "", "", "", ""],
    ["課程平台", "尚未發布", "", "", "", ""],
    ["Email提醒", "尚未發布", "", "學生登錄人數", "0", ""],
    ["LMS/社群", "尚未發布", "", "訂閱週三報人數", "0", ""],
    ["協作平台", "尚未發布", "", "有效 Email 人數", "0", ""],
    ["", "", "", "", "", ""],
    ["本週流程步驟", "", "", "", "", ""],
    ["1", "建立週次資料", "⚪ 待處理", "下一步", "建立或確認本週課程重點資料列", ""],
    ["2", "填寫課程重點", "⚪ 待處理", "", "補齊核心概念、活動、作業與下週預告", ""],
    ["3", "產生三端內容", "⚪ 待處理", "", "產生課程平台公告、Email、LMS/社群文案", ""],
    ["4", "建立 Docs / 素材資料夾", "⚪ 待處理", "", "建立正式 Docs 與課程素材資料夾", ""],
    ["5", "發布三端", "⚪ 待處理", "", "複製文案到課程平台、Email、LMS/社群後標記", ""],
    ["6", "發布協作平台", "⚪ 待處理", "", "送出 Google Chat 協作通知", ""],
  ];

  sheet.getRange(1, 1, values.length, 6).setValues(values);
  sheet.getRange("A1:F1").merge();
  sheet.getRange("B2:F2").merge();
  sheet.getRange("A8:F8").merge();
  sheet.getRange("A12:F12").merge();
  sheet.getRange("A18:F18").merge();
  sheet.getRange("A1")
    .setFontSize(18)
    .setFontWeight("bold")
    .setFontColor("#ffffff")
    .setHorizontalAlignment("center")
    .setBackground("#243b53");
  sheet.getRange("A2:F2").setBackground("#edf4fb");
  sheet.getRange("A4:A6").setFontWeight("bold");
  sheet.getRange("D4:D6").setFontWeight("bold");
  sheet.getRange("A8:F8").setFontWeight("bold").setBackground("#d9eaf7");
  sheet.getRange("A12:F12").setFontWeight("bold").setBackground("#d9eaf7");
  sheet.getRange("A18:F18").setFontWeight("bold").setBackground("#d9eaf7");
  sheet.getRange("A9:F10").setBackground("#f8fafc");
  sheet.getRange("A13:B16").setBackground("#f8fafc");
  sheet.getRange("D14:E16").setBackground("#eef7ff");
  sheet.getRange("D14:D16").setFontWeight("bold");
  sheet.getRange("A19:F24").setBackground("#f8fafc");
  sheet.getRange("E4").setFontWeight("bold");
  sheet.setColumnWidth(1, 150);
  sheet.setColumnWidth(2, 250);
  sheet.setColumnWidth(3, 150);
  sheet.setColumnWidth(4, 150);
  sheet.setColumnWidth(5, 300);
  sheet.setColumnWidth(6, 40);
  sheet.setFrozenRows(1);
}

function UCR_setupWeeklyDataSheet_(ss) {
  const sheet = UCR_getOrCreateSheet_(ss, UCR_CONFIG.SHEETS.WEEKLY_DATA);
  sheet.clear();
  const headers = [
    "週次ID",
    "日期區間",
    "課程單元主題",
    "本週核心概念",
    "課堂活動與討論",
    "指定閱讀與作業",
    "下週預告",
    "補充資源",
    "課程素材資料夾URL",
    "建立時間",
    "最後更新時間",
  ];
  sheet.getRange(1, 1, 1, headers.length).setValues([headers]);
  UCR_styleHeader_(sheet, headers.length);
  sheet.setFrozenRows(1);
  sheet.setColumnWidths(1, headers.length, 180);
  sheet.setColumnWidth(3, 260);
  sheet.setColumnWidth(4, 320);
  sheet.setColumnWidth(5, 320);
  sheet.setColumnWidth(6, 320);
  sheet.setColumnWidth(7, 300);
  sheet.setColumnWidth(8, 300);
}

function UCR_setupCopyOutputSheet_(ss) {
  const sheet = UCR_getOrCreateSheet_(ss, UCR_CONFIG.SHEETS.COPY_OUTPUT);
  sheet.clear();
  const headers = ["週次ID", "課程平台公告版", "Email短提醒版", "LMS社群貼文版", "產生時間", "是否確認"];
  sheet.getRange(1, 1, 1, headers.length).setValues([headers]);
  UCR_styleHeader_(sheet, headers.length);
  sheet.setFrozenRows(1);
  sheet.setColumnWidth(1, 150);
  sheet.setColumnWidth(2, 440);
  sheet.setColumnWidth(3, 380);
  sheet.setColumnWidth(4, 420);
}

function UCR_setupPublishLogSheet_(ss) {
  const sheet = UCR_getOrCreateSheet_(ss, UCR_CONFIG.SHEETS.PUBLISH_LOG);
  sheet.clear();
  const headers = ["週次ID", "課程平台", "Email", "LMS/社群", "協作平台", "Google Docs URL", "發布備註", "最後更新時間"];
  sheet.getRange(1, 1, 1, headers.length).setValues([headers]);
  UCR_styleHeader_(sheet, headers.length);
  sheet.setFrozenRows(1);
  sheet.setColumnWidths(1, headers.length, 180);
  sheet.setColumnWidth(6, 360);
}

function UCR_setupSettingsSheet_(ss) {
  const sheet = UCR_getOrCreateSheet_(ss, UCR_CONFIG.SHEETS.SETTINGS);
  sheet.clear();
  sheet.getRange(1, 1, UCR_CONFIG.DEFAULT_SETTINGS.length, 3).setValues(UCR_CONFIG.DEFAULT_SETTINGS);
  UCR_styleHeader_(sheet, 3);
  sheet.setFrozenRows(1);
  sheet.setColumnWidth(1, 220);
  sheet.setColumnWidth(2, 360);
  sheet.setColumnWidth(3, 460);
}

function UCR_setupIndexSheet_(ss) {
  const sheet = UCR_getOrCreateSheet_(ss, UCR_CONFIG.SHEETS.INDEX);
  sheet.clear();
  const headers = ["週次ID", "日期區間", "摘要", "Google Docs", "素材資料夾", "課程平台", "發布狀態", "最後更新時間"];
  sheet.getRange(1, 1, 1, headers.length).setValues([headers]);
  UCR_styleHeader_(sheet, headers.length);
  sheet.setFrozenRows(1);
  sheet.setColumnWidths(1, headers.length, 180);
  sheet.setColumnWidth(3, 460);
  sheet.setColumnWidth(7, 260);
}

function UCR_setupPlatformMaterialsSheet_(ss) {
  const sheet = UCR_getOrCreateSheet_(ss, UCR_CONFIG.SHEETS.PLATFORM_MATERIALS);
  sheet.clear();
  const headers = ["週次ID", "平台標題", "摘要", "純文字版", "HTML貼上版", "Google Docs連結", "素材資料夾連結", "產生時間"];
  sheet.getRange(1, 1, 1, headers.length).setValues([headers]);
  UCR_styleHeader_(sheet, headers.length);
  sheet.setFrozenRows(1);
  sheet.setColumnWidth(1, 150);
  sheet.setColumnWidth(2, 300);
  sheet.setColumnWidth(3, 380);
  sheet.setColumnWidth(4, 520);
  sheet.setColumnWidth(5, 520);
}

function UCR_setupStudentsSheet_(ss) {
  const sheet = UCR_getOrCreateSheet_(ss, UCR_CONFIG.SHEETS.STUDENTS);
  sheet.clear();
  const headers = UCR_getStudentSheetHeaders_();
  sheet.getRange(1, 1, 1, headers.length).setValues([headers]);
  UCR_styleHeader_(sheet, headers.length);
  sheet.setFrozenRows(1);
  sheet.setColumnWidth(1, 180);
  sheet.setColumnWidth(2, 140);
  sheet.setColumnWidth(3, 140);
  sheet.setColumnWidth(4, 260);
  sheet.setColumnWidth(5, 180);
  sheet.setColumnWidth(6, 260);
  sheet.setColumnWidth(7, 150);
  sheet.setColumnWidth(8, 180);
  sheet.getRange("D:D").setNumberFormat("@");
  sheet.getRange("G:G").setNumberFormat("@");
  const rule = SpreadsheetApp.newDataValidation().requireValueInList(["是", "否"], true).setAllowInvalid(false).build();
  sheet.getRange(2, 7, Math.max(sheet.getMaxRows() - 1, 1), 1).setDataValidation(rule);
  UCR_fillDefaultStudentSubscription_(sheet);
}

function UCR_ensureStructure_(ss) {
  UCR_ensureSettingsRows_(ss);
  UCR_ensurePublishLogColumns_(ss);
  UCR_ensurePlatformMaterialsSheet_(ss);
  UCR_ensureStudentsSheet_(ss);
}

function UCR_ensureSettingsRows_(ss) {
  const sheet = ss.getSheetByName(UCR_CONFIG.SHEETS.SETTINGS);
  if (!sheet) return;
  const lastRow = sheet.getLastRow();
  const existingKeys =
    lastRow >= 2
      ? sheet.getRange(2, 1, lastRow - 1, 1).getValues().map(function (row) {
          return row[0];
        })
      : [];
  const missingRows = UCR_CONFIG.DEFAULT_SETTINGS.slice(1).filter(function (row) {
    return existingKeys.indexOf(row[0]) === -1;
  });
  if (missingRows.length > 0) {
    sheet.getRange(sheet.getLastRow() + 1, 1, missingRows.length, 3).setValues(missingRows);
  }
}

function UCR_ensurePublishLogColumns_(ss) {
  const sheet = ss.getSheetByName(UCR_CONFIG.SHEETS.PUBLISH_LOG);
  if (!sheet) return;
  const headers = ["週次ID", "課程平台", "Email", "LMS/社群", "協作平台", "Google Docs URL", "發布備註", "最後更新時間"];
  sheet.getRange(1, 1, 1, headers.length).setValues([headers]);
  UCR_styleHeader_(sheet, headers.length);
}

function UCR_ensurePlatformMaterialsSheet_(ss) {
  const sheet = ss.getSheetByName(UCR_CONFIG.SHEETS.PLATFORM_MATERIALS);
  if (!sheet) {
    UCR_setupPlatformMaterialsSheet_(ss);
  }
}

function UCR_ensureStudentsSheet_(ss) {
  let sheet = ss.getSheetByName(UCR_CONFIG.SHEETS.STUDENTS);
  if (!sheet) {
    UCR_setupStudentsSheet_(ss);
    return;
  }
  const headers = UCR_getStudentSheetHeaders_();
  const lastColumn = Math.max(sheet.getLastColumn(), headers.length);
  const existingHeaders = sheet.getRange(1, 1, 1, lastColumn).getValues()[0];
  const missingHeaders = headers.filter(function (header) {
    return existingHeaders.indexOf(header) === -1;
  });
  if (missingHeaders.length > 0 || lastColumn < headers.length) {
    sheet.getRange(1, 1, 1, headers.length).setValues([headers]);
  }
  UCR_styleHeader_(sheet, headers.length);
  sheet.setFrozenRows(1);
  sheet.setColumnWidth(1, 180);
  sheet.setColumnWidth(2, 140);
  sheet.setColumnWidth(3, 140);
  sheet.setColumnWidth(4, 260);
  sheet.setColumnWidth(5, 180);
  sheet.setColumnWidth(6, 260);
  sheet.setColumnWidth(7, 150);
  sheet.setColumnWidth(8, 180);
  sheet.getRange("D:D").setNumberFormat("@");
  sheet.getRange("G:G").setNumberFormat("@");
  const rule = SpreadsheetApp.newDataValidation().requireValueInList(["是", "否"], true).setAllowInvalid(false).build();
  sheet.getRange(2, 7, Math.max(sheet.getMaxRows() - 1, 1), 1).setDataValidation(rule);
}

function UCR_getStudentSheetHeaders_() {
  return ["登錄時間", "學號", "姓名", "Email", "系級/班級", "備註", "是否訂閱週三報", "最後更新時間"];
}

function UCR_fillDefaultStudentSubscription_(sheet) {
  if (!sheet || sheet.getLastRow() < 2) return;
  const range = sheet.getRange(2, 1, sheet.getLastRow() - 1, 8);
  const values = range.getValues();
  let changed = false;
  values.forEach(function (row) {
    const hasStudentData = Boolean(row[1] || row[2] || row[3]);
    if (hasStudentData && !row[6]) {
      row[6] = "是";
      changed = true;
    }
  });
  if (changed) range.setValues(values);
}

function UCR_createCurrentWeekReport() {
  UCR_runAction_("建立本週課程重點範本", function () {
    const result = UCR_createWeekReportByInfo_(UCR_getCurrentWeekInfo_());
    if (!result) return;
    UCR_alert_(result.existed ? "本週課程重點已存在:" + result.weekId : "已建立本週課程重點範本:" + result.weekId);
  });
}

function UCR_createNextWeekReport() {
  UCR_runAction_("建立下一週課程重點範本", function () {
    const result = UCR_createWeekReportByInfo_(UCR_getNextWeekInfo_());
    if (!result) return;
    UCR_alert_(result.existed ? "下一週課程重點已存在:" + result.weekId : "已建立下一週課程重點範本:" + result.weekId);
  });
}

function UCR_createWeekReportByInfo_(weekInfo) {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const sheet = ss.getSheetByName(UCR_CONFIG.SHEETS.WEEKLY_DATA);
  if (!sheet) {
    UCR_alert_("請先執行「初始化平台」。");
    return null;
  }
  const weekId = weekInfo.weekId;
  const dateRange = weekInfo.dateRange;
  if (UCR_findRowByWeekId_(sheet, weekId) > 0) {
    UCR_refreshDashboardForWeek_(weekId);
    return { weekId: weekId, dateRange: dateRange, existed: true };
  }
  const now = new Date();
  const p = UCR_CONFIG.PLACEHOLDERS;
  sheet.appendRow([
    weekId,
    dateRange,
    p.topic,
    p.concepts,
    p.activities,
    p.assignments,
    p.nextPreview,
    p.resources,
    "",
    now,
    now,
  ]);
  UCR_ensurePublishLogRow_(weekId);
  UCR_refreshDashboardForWeek_(weekId);
  UCR_updateWeeklyIndex_();
  return { weekId: weekId, dateRange: dateRange, existed: false };
}

function UCR_getCurrentWeekInfo_() {
  return UCR_getWeekInfoForDate_(new Date());
}

function UCR_getNextWeekInfo_() {
  const nextWeek = new Date();
  nextWeek.setDate(nextWeek.getDate() + 7);
  return UCR_getWeekInfoForDate_(nextWeek);
}

function UCR_getWeekInfoForDate_(today) {
  const timezone = UCR_CONFIG.TIMEZONE;
  const day = today.getDay();
  const diffToMonday = day === 0 ? -6 : 1 - day;
  const monday = new Date(today);
  monday.setDate(today.getDate() + diffToMonday);
  const friday = new Date(monday);
  friday.setDate(monday.getDate() + 4);
  const year = Utilities.formatDate(today, timezone, "yyyy");
  const weekNumber = UCR_getWeekNumber_(today);
  return {
    weekId: year + "-W" + String(weekNumber).padStart(2, "0"),
    dateRange:
      Utilities.formatDate(monday, timezone, "yyyy/MM/dd") +
      "–" +
      Utilities.formatDate(friday, timezone, "yyyy/MM/dd"),
  };
}

function UCR_getWeekNumber_(date) {
  const target = new Date(date.valueOf());
  const dayNumber = (date.getDay() + 6) % 7;
  target.setDate(target.getDate() - dayNumber + 3);
  const firstThursday = new Date(target.getFullYear(), 0, 4);
  const firstDayNumber = (firstThursday.getDay() + 6) % 7;
  firstThursday.setDate(firstThursday.getDate() - firstDayNumber + 3);
  return 1 + Math.round((target - firstThursday) / (7 * 24 * 60 * 60 * 1000));
}

function UCR_createMaterialsFolderForActiveWeek() {
  UCR_runAction_("建立本週課程素材資料夾", function () {
    const weekId = UCR_getActiveWeekId_();
    if (!weekId) {
      UCR_alert_("目前沒有週次ID,請先建立本週課程重點範本。");
      return;
    }
    const url = UCR_createMaterialsFolderForWeek_(weekId);
    if (!url) return;
    UCR_refreshDashboardForWeek_(weekId);
    UCR_updateWeeklyIndex_();
    UCR_alert_("已取得本週課程素材資料夾:\n" + url);
  });
}

function UCR_createMaterialsFolderForWeek_(weekId) {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const sheet = ss.getSheetByName(UCR_CONFIG.SHEETS.WEEKLY_DATA);
  if (!sheet) {
    UCR_alert_("請先執行「初始化平台」。");
    return "";
  }
  const row = UCR_findRowByWeekId_(sheet, weekId);
  if (row <= 0) {
    UCR_alert_("找不到課程週次資料:" + weekId);
    return "";
  }
  const existingUrl = sheet.getRange(row, 9).getValue();
  if (existingUrl) return existingUrl;
  const settings = UCR_getSettings_();
  const courseName = settings["課程名稱"] || "課程";
  const root = UCR_getOrCreateRootFolder_(settings);
  const folder = root.createFolder(weekId + "_" + courseName + "_課程素材");
  const url = folder.getUrl();
  sheet.getRange(row, 9).setValue(url);
  sheet.getRange(row, 11).setValue(new Date());
  return url;
}

function UCR_getOrCreateRootFolder_(settings) {
  const rootFolderId = settings["Drive根資料夾ID"];
  if (rootFolderId) {
    try {
      return DriveApp.getFolderById(rootFolderId);
    } catch (e) {
      UCR_alert_("「Drive根資料夾ID」無法讀取,系統將自動建立新的根資料夾。\n\n錯誤:" + e.message);
    }
  }
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const folder = DriveApp.createFolder(ss.getName() + "_大學課程內容重點資料夾");
  const settingsSheet = ss.getSheetByName(UCR_CONFIG.SHEETS.SETTINGS);
  const row = UCR_findSettingsRow_("Drive根資料夾ID");
  if (settingsSheet && row > 0) settingsSheet.getRange(row, 2).setValue(folder.getId());
  return folder;
}

function UCR_generateThreeChannelCopies() {
  UCR_runAction_("產生三端內容", function () {
    const weekId = UCR_getActiveWeekId_();
    if (!weekId) {
      UCR_alert_("目前沒有週次ID,請先建立本週課程重點範本。");
      return;
    }
    const result = UCR_generateThreeChannelCopiesForWeek_(weekId);
    if (!result) return;
    let message = "已產生三端內容。請到「02_三端內容輸出」或發布面板查看。";
    if (result.warnings.length > 0) {
      message += "\n\n提醒:下列欄位仍是預設提示文字或尚未填寫:\n- " + result.warnings.join("\n- ");
    }
    UCR_alert_(message);
  });
}

function UCR_generateThreeChannelCopiesForWeek_(weekId) {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const outputSheet = ss.getSheetByName(UCR_CONFIG.SHEETS.COPY_OUTPUT);
  if (!outputSheet) {
    UCR_alert_("請先執行「初始化平台」。");
    return null;
  }
  const data = UCR_getWeeklyDataByWeekId_(weekId);
  if (!data) {
    UCR_alert_("找不到課程週次資料:" + weekId);
    return null;
  }
  const settings = UCR_getSettings_();
  const platformCopy = UCR_buildCoursePlatformCopy_(data, settings);
  const emailCopy = UCR_buildEmailCopy_(data, settings);
  const lmsCopy = UCR_buildLmsCopy_(data, settings);
  const now = new Date();
  const existingRow = UCR_findRowByWeekId_(outputSheet, weekId);
  if (existingRow > 0) {
    outputSheet.getRange(existingRow, 2, 1, 5).setValues([[platformCopy, emailCopy, lmsCopy, now, "否"]]);
  } else {
    outputSheet.appendRow([weekId, platformCopy, emailCopy, lmsCopy, now, "否"]);
  }
  UCR_refreshDashboardForWeek_(weekId);
  UCR_updateWeeklyIndex_();
  return {
    warnings: UCR_getIncompleteFieldWarnings_(data),
  };
}

function UCR_buildCoursePlatformCopy_(data, settings) {
  const courseName = settings["課程名稱"] || "課程";
  const instructor = settings["授課教師"] || "授課教師";
  return [
    "【" + courseName + "|" + data.weekId + " 課程內容重點】",
    "",
    "日期區間:" + data.dateRange,
    "",
    "一、課程單元主題",
    UCR_normalizeText_(data.topic),
    "",
    "二、本週核心概念",
    UCR_normalizeText_(data.concepts),
    "",
    "三、課堂活動與討論",
    UCR_normalizeText_(data.activities),
    "",
    "四、指定閱讀與作業",
    UCR_normalizeText_(data.assignments),
    "",
    "五、下週預告",
    UCR_normalizeText_(data.nextPreview),
    "",
    "六、補充資源",
    UCR_normalizeText_(data.resources),
    data.materialsFolderUrl ? "\n課程素材資料夾:" + data.materialsFolderUrl : "",
    "",
    instructor,
  ].join("\n").trim();
}

function UCR_buildEmailCopy_(data, settings) {
  const courseName = settings["課程名稱"] || "課程";
  const platformUrl = settings["課程平台網址"] || "";
  return [
    "主旨:【" + courseName + "】" + data.weekId + " 課程重點與下週提醒",
    "",
    "同學好,以下是本週課程重點與需留意事項:",
    "",
    "課程主題:",
    UCR_compactText_(data.topic),
    "",
    "核心概念:",
    UCR_compactText_(data.concepts),
    "",
    "指定閱讀與作業:",
    UCR_compactText_(data.assignments),
    "",
    "下週預告:",
    UCR_compactText_(data.nextPreview),
    "",
    platformUrl ? "課程平台:" + platformUrl : "請留意課程平台後續公告。",
  ].join("\n");
}

function UCR_buildLmsCopy_(data, settings) {
  const courseName = settings["課程名稱"] || "課程";
  return [
    "【" + courseName + "|" + data.weekId + " 學習重點】",
    "",
    "本週主題:" + UCR_compactText_(data.topic),
    "",
    "你需要掌握的重點:",
    UCR_compactText_(data.concepts),
    "",
    "課堂討論/活動:",
    UCR_compactText_(data.activities),
    "",
    "請完成:",
    UCR_compactText_(data.assignments),
    "",
    data.materialsFolderUrl ? "課程素材:" + data.materialsFolderUrl : "",
  ].join("\n").trim();
}

function UCR_generateGoogleDocForActiveWeek() {
  UCR_runAction_("產生 Google Docs 課程重點", function () {
    const weekId = UCR_getActiveWeekId_();
    if (!weekId) {
      UCR_alert_("目前沒有週次ID,請先建立本週課程重點範本。");
      return;
    }
    const docUrl = UCR_createGoogleDocForWeek_(weekId);
    if (!docUrl) return;
    UCR_refreshDashboardForWeek_(weekId);
    UCR_updateWeeklyIndex_();
    UCR_alert_("已產生 Google Docs 課程重點:\n" + docUrl);
  });
}

function UCR_createGoogleDocForWeek_(weekId) {
  const existingUrl = UCR_getPublishInfo_(weekId).docUrl;
  if (existingUrl) return existingUrl;
  const data = UCR_getWeeklyDataByWeekId_(weekId);
  if (!data) {
    UCR_alert_("找不到課程週次資料:" + weekId);
    return "";
  }
  const settings = UCR_getSettings_();
  const courseName = settings["課程名稱"] || "課程";
  const doc = DocumentApp.create(courseName + "_" + weekId + "_課程內容重點");
  const body = doc.getBody();
  body.clear();
  body.setMarginTop(54).setMarginBottom(54).setMarginLeft(54).setMarginRight(54);
  UCR_buildFormalDocBody_(body, data, settings);
  UCR_buildFormalDocFooter_(doc, settings);
  doc.saveAndClose();
  const url = doc.getUrl();
  UCR_updatePublishLogDocUrl_(weekId, url);
  return url;
}

function UCR_buildFormalDocBody_(body, data, settings) {
  const courseName = settings["課程名稱"] || "課程";
  const instructor = settings["授課教師"] || "授課教師";
  const department = settings["開課系所"] || "";
  body.appendParagraph(courseName + "|課程內容重點").setHeading(DocumentApp.ParagraphHeading.TITLE).setAlignment(DocumentApp.HorizontalAlignment.CENTER);
  const meta = body.appendParagraph(data.weekId + "|" + data.dateRange).setAlignment(DocumentApp.HorizontalAlignment.CENTER);
  UCR_setParagraphTextColor_(meta, "#666666");
  if (department) {
    const dep = body.appendParagraph(department).setAlignment(DocumentApp.HorizontalAlignment.CENTER);
    UCR_setParagraphTextColor_(dep, "#666666");
  }
  body.appendHorizontalRule();
  UCR_appendDocSection_(body, "一、課程單元主題", data.topic);
  UCR_appendDocSection_(body, "二、本週核心概念", data.concepts);
  UCR_appendDocSection_(body, "三、課堂活動與討論", data.activities);
  UCR_appendDocSection_(body, "四、指定閱讀與作業", data.assignments);
  UCR_appendDocSection_(body, "五、下週預告", data.nextPreview);
  UCR_appendDocSection_(body, "六、補充資源", data.resources);
  if (data.materialsFolderUrl) {
    body.appendParagraph("課程素材資料夾").setHeading(DocumentApp.ParagraphHeading.HEADING2);
    const p = body.appendParagraph("");
    p.appendText(data.materialsFolderUrl).setLinkUrl(data.materialsFolderUrl);
  }
  body.appendHorizontalRule();
  body.appendParagraph(instructor).setAlignment(DocumentApp.HorizontalAlignment.RIGHT);
}

function UCR_appendDocSection_(body, heading, text) {
  body.appendParagraph(heading).setHeading(DocumentApp.ParagraphHeading.HEADING2);
  UCR_normalizeText_(text).split("\n").forEach(function (line) {
    if (line.trim()) body.appendParagraph(line.trim()).setLineSpacing(1.15);
  });
}

function UCR_buildFormalDocFooter_(doc, settings) {
  const footer = doc.addFooter();
  const p = footer.appendParagraph((settings["課程名稱"] || "課程") + "|" + (settings["授課教師"] || "授課教師"));
  p.setAlignment(DocumentApp.HorizontalAlignment.CENTER);
  UCR_setParagraphTextColor_(p, "#888888");
}

function UCR_generatePlatformPublishMaterials() {
  UCR_runAction_("產生課程平台發布素材", function () {
    const weekId = UCR_getActiveWeekId_();
    if (!weekId) {
      UCR_alert_("目前沒有週次ID,請先建立本週課程重點範本。");
      return;
    }
    const result = UCR_generatePlatformPublishMaterialsForWeek_(weekId);
    if (!result) return;
    UCR_alert_("已產生課程平台發布素材,請到「06_課程平台發布素材」查看。");
  });
}

function UCR_generatePlatformPublishMaterialsForWeek_(weekId) {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  UCR_ensurePlatformMaterialsSheet_(ss);
  const data = UCR_getWeeklyDataByWeekId_(weekId);
  if (!data) {
    UCR_alert_("找不到課程週次資料:" + weekId);
    return null;
  }
  const settings = UCR_getSettings_();
  const publishInfo = UCR_getPublishInfo_(weekId);
  const copies = UCR_getThreeChannelCopiesForWeek_(weekId);
  const plain = copies.platform || UCR_buildCoursePlatformCopy_(data, settings);
  const title = UCR_buildPlatformMaterialTitle_(data, settings);
  const summary = UCR_makeWeeklySummary_(data);
  const html = UCR_buildPlatformHtml_(title, summary, plain, publishInfo.docUrl, data.materialsFolderUrl);
  const sheet = ss.getSheetByName(UCR_CONFIG.SHEETS.PLATFORM_MATERIALS);
  const rowValues = [weekId, title, summary, plain, html, publishInfo.docUrl || "", data.materialsFolderUrl || "", new Date()];
  const existingRow = UCR_findRowByWeekId_(sheet, weekId);
  let row = existingRow;
  if (existingRow > 0) {
    sheet.getRange(existingRow, 1, 1, rowValues.length).setValues([rowValues]);
  } else {
    sheet.appendRow(rowValues);
    row = sheet.getLastRow();
  }
  UCR_setLinkCell_(sheet, row, 6, "開啟 Docs", publishInfo.docUrl, "尚未建立");
  UCR_setLinkCell_(sheet, row, 7, "開啟素材資料夾", data.materialsFolderUrl, "尚未建立");
  return { title: title, summary: summary, plainText: plain, html: html };
}

function UCR_buildPlatformMaterialTitle_(data, settings) {
  return (settings["課程名稱"] || "課程") + "|" + data.weekId + " 課程內容重點";
}

function UCR_buildPlatformHtml_(title, summary, plainText, docUrl, folderUrl) {
  const paragraphs = UCR_escapeHtml_(plainText)
    .split(/\n{2,}/)
    .map(function (block) {
      return "<p>" + block.replace(/\n/g, "<br>") + "</p>";
    })
    .join("\n");
  const links = [];
  if (docUrl) links.push('<li><a href="' + UCR_escapeHtml_(docUrl) + '">Google Docs 課程重點</a></li>');
  if (folderUrl) links.push('<li><a href="' + UCR_escapeHtml_(folderUrl) + '">課程素材資料夾</a></li>');
  return [
    '<article class="course-weekly-summary">',
    "  <h1>" + UCR_escapeHtml_(title) + "</h1>",
    '  <p class="summary">' + UCR_escapeHtml_(summary) + "</p>",
    '  <div class="body">',
    paragraphs,
    "  </div>",
    links.length ? '  <ul class="links">' + links.join("") + "</ul>" : "",
    "</article>",
  ].filter(function (line) {
    return line !== "";
  }).join("\n");
}

function UCR_publishToCollaborationPlatform() {
  UCR_runAction_("一鍵發布到協作平台", function () {
    const weekId = UCR_getActiveWeekId_();
    if (!weekId) {
      UCR_alert_("目前沒有週次ID,請先建立本週課程重點範本。");
      return;
    }
    const settings = UCR_getSettings_();
    if (String(settings["是否啟用協作平台發布"] || "").trim() !== "是") {
      UCR_alert_("尚未啟用協作平台發布。請到「04_設定」將「是否啟用協作平台發布」填為「是」。");
      return;
    }
    const webhookUrl = String(settings["Google Chat Webhook URL"] || "").trim();
    if (!webhookUrl) {
      UCR_alert_("尚未設定 Google Chat Webhook URL。");
      return;
    }
    if (!UCR_hasThreeChannelCopies_(weekId)) {
      UCR_alert_("三端內容尚未產生。請先執行「產生三端內容」。");
      return;
    }
    let data = UCR_getWeeklyDataByWeekId_(weekId);
    let publishInfo = UCR_getPublishInfo_(weekId);
    let docUrl = publishInfo.docUrl;
    if (!docUrl) {
      if (!UCR_confirm_("Google Docs 尚未建立", "是否立即建立 Google Docs 課程重點後再發布?")) return;
      docUrl = UCR_createGoogleDocForWeek_(weekId);
    }
    let folderUrl = data.materialsFolderUrl;
    if (!folderUrl) {
      if (!UCR_confirm_("素材資料夾尚未建立", "是否立即建立本週課程素材資料夾後再發布?")) return;
      folderUrl = UCR_createMaterialsFolderForWeek_(weekId);
      data = UCR_getWeeklyDataByWeekId_(weekId);
    }
    UCR_generatePlatformPublishMaterialsForWeek_(weekId);
    publishInfo = UCR_getPublishInfo_(weekId);
    const message = UCR_buildGoogleChatMessage_(weekId, data, settings, publishInfo, docUrl, folderUrl);
    UCR_sendGoogleChatWebhook_(webhookUrl, message);
    UCR_updatePublishLogCollaborationStatus_(weekId, settings["協作平台名稱"] || "Google Chat");
    UCR_refreshDashboardForWeek_(weekId);
    UCR_updateWeeklyIndex_();
    UCR_alert_("已發布課程內容重點到協作平台。");
  });
}

function UCR_buildGoogleChatMessage_(weekId, data, settings, publishInfo, docUrl, folderUrl) {
  return [
    "【" + (settings["課程名稱"] || "課程") + "|" + weekId + " 課程內容重點】",
    "日期區間:" + data.dateRange,
    "",
    "摘要:",
    UCR_makeWeeklySummary_(data),
    "",
    "Google Docs:" + (docUrl || "尚未建立"),
    "課程素材:" + (folderUrl || "尚未建立"),
    "",
    "發布狀態:",
    "課程平台:" + (publishInfo.platformStatus || "尚未發布"),
    "Email:" + (publishInfo.emailStatus || "尚未發布"),
    "LMS/社群:" + (publishInfo.lmsStatus || "尚未發布"),
  ].join("\n");
}

function UCR_sendGoogleChatWebhook_(webhookUrl, message) {
  const response = UrlFetchApp.fetch(webhookUrl, {
    method: "post",
    contentType: "application/json; charset=UTF-8",
    payload: JSON.stringify({ text: message }),
    muteHttpExceptions: true,
  });
  const code = response.getResponseCode();
  if (code < 200 || code >= 300) {
    throw new Error("Google Chat Webhook 發送失敗,HTTP " + code + ":" + response.getContentText());
  }
}

function UCR_showWeeklyInputForm() {
  UCR_runAction_("開啟課程週次填寫表單", function () {
    const html = HtmlService.createHtmlOutput(UCR_buildWeeklyInputFormHtml_()).setTitle("課程週次填寫表單");
    SpreadsheetApp.getUi().showSidebar(html);
  });
}

function UCR_getWeeklyInputFormData() {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  UCR_ensureStructure_(ss);
  const weekId = UCR_getActiveWeekId_() || UCR_getLatestWeekId_();
  const settings = UCR_getSettings_();
  if (!weekId) {
    return {
      hasWeek: false,
      weekId: "",
      dateRange: "",
      courseName: settings["課程名稱"] || "課程",
      fields: UCR_getEmptyFormFields_(),
      warnings: [],
    };
  }
  const data = UCR_getWeeklyDataByWeekId_(weekId);
  if (!data) {
    return {
      hasWeek: false,
      weekId: "",
      dateRange: "",
      courseName: settings["課程名稱"] || "課程",
      fields: UCR_getEmptyFormFields_(),
      warnings: [],
    };
  }
  return {
    hasWeek: true,
    weekId: weekId,
    dateRange: data.dateRange,
    courseName: settings["課程名稱"] || "課程",
    fields: {
      topic: data.topic || "",
      concepts: data.concepts || "",
      activities: data.activities || "",
      assignments: data.assignments || "",
      nextPreview: data.nextPreview || "",
      resources: data.resources || "",
    },
    warnings: UCR_getIncompleteFieldWarnings_(data),
  };
}

function UCR_getEmptyFormFields_() {
  const p = UCR_CONFIG.PLACEHOLDERS;
  return {
    topic: p.topic,
    concepts: p.concepts,
    activities: p.activities,
    assignments: p.assignments,
    nextPreview: p.nextPreview,
    resources: p.resources,
  };
}

function UCR_saveWeeklyInputForm(formData) {
  return UCR_runFormAction_("儲存課程週次資料", function () {
    return UCR_saveWeeklyInputForm_(formData, false);
  });
}

function UCR_saveWeeklyInputFormAndGenerate(formData) {
  return UCR_runFormAction_("儲存並產生三端內容", function () {
    const result = UCR_saveWeeklyInputForm_(formData, true);
    if (!result.ok) return result;
    const generateResult = UCR_generateThreeChannelCopiesForWeek_(result.weekId);
    return {
      ok: true,
      message:
        "已儲存課程週次資料,並產生三端內容。" +
        (generateResult && generateResult.warnings.length
          ? "\n仍需確認欄位:" + generateResult.warnings.join("、")
          : ""),
    };
  });
}

function UCR_saveWeeklyInputForm_(formData, allowCreateIfMissing) {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const sheet = ss.getSheetByName(UCR_CONFIG.SHEETS.WEEKLY_DATA);
  if (!sheet) {
    return { ok: false, message: "請先執行「初始化平台」。" };
  }
  const weekId = String(formData && formData.weekId ? formData.weekId : UCR_getActiveWeekId_()).trim();
  if (!weekId) {
    if (!allowCreateIfMissing) {
      return { ok: false, message: "目前沒有週次ID,請先建立本週課程重點範本。" };
    }
    const created = UCR_createWeekReportByInfo_(UCR_getCurrentWeekInfo_());
    if (!created) return { ok: false, message: "無法建立本週課程重點範本。" };
    return UCR_saveWeeklyInputForm_(
      Object.assign({}, formData || {}, { weekId: created.weekId }),
      false,
    );
  }
  const row = UCR_findRowByWeekId_(sheet, weekId);
  if (row <= 0) {
    return { ok: false, message: "找不到課程週次資料:" + weekId };
  }
  const values = [
    String(formData.topic || "").trim(),
    String(formData.concepts || "").trim(),
    String(formData.activities || "").trim(),
    String(formData.assignments || "").trim(),
    String(formData.nextPreview || "").trim(),
    String(formData.resources || "").trim(),
    new Date(),
  ];
  sheet.getRange(row, 3, 1, 6).setValues([values.slice(0, 6)]);
  sheet.getRange(row, 11).setValue(values[6]);
  UCR_refreshDashboardForWeek_(weekId);
  UCR_updateWeeklyIndex_();
  const updated = UCR_getWeeklyDataByWeekId_(weekId);
  const warnings = UCR_getIncompleteFieldWarnings_(updated);
  return {
    ok: true,
    weekId: weekId,
    message:
      "已儲存課程週次資料。" +
      (warnings.length ? "\n仍需確認欄位:" + warnings.join("、") : ""),
  };
}

function UCR_runFormAction_(actionName, callback) {
  try {
    UCR_ensureStructure_(SpreadsheetApp.getActiveSpreadsheet());
    return callback();
  } catch (e) {
    Logger.log(e && e.stack ? e.stack : e);
    return {
      ok: false,
      message: actionName + "失敗:" + (e && e.message ? e.message : e),
    };
  }
}

function UCR_buildWeeklyInputFormHtml_() {
  return [
    "<!doctype html>",
    "<html><head><base target=\"_top\"><style>",
    "body{margin:0;background:#f6f8fb;color:#182230;font-family:Arial,'Noto Sans TC',sans-serif;font-size:13px}.app{padding:14px}h1{font-size:18px;margin:0 0 4px;color:#102a43}.muted{color:#667085;font-size:12px;line-height:1.45}.notice{border:1px solid #f6d58f;background:#fff8e5;border-radius:8px;padding:10px;margin:10px 0;color:#7a4b00;line-height:1.5}.panel{background:#fff;border:1px solid #d9e2ec;border-radius:8px;padding:12px;margin:10px 0;box-shadow:0 1px 2px rgba(16,42,67,.04)}label{display:block;font-weight:700;color:#243b53;margin:12px 0 6px}input,textarea{box-sizing:border-box;width:100%;border:1px solid #cbd5e1;border-radius:8px;padding:10px;font-size:13px;line-height:1.5;background:#fff;color:#111827}textarea{min-height:104px;resize:vertical}.short{min-height:74px}.actions{display:grid;grid-template-columns:1fr;gap:8px;margin-top:12px}.two{grid-template-columns:1fr 1fr}button{border:1px solid #b8c7d9;border-radius:8px;background:#fff;color:#17324d;font-weight:700;font-size:12px;padding:10px;cursor:pointer}.primary{background:#243b53;border-color:#243b53;color:#fff}.success{background:#087443;border-color:#087443;color:#fff}button:disabled{opacity:.55;cursor:wait}#toast{position:sticky;bottom:0;margin-top:10px;background:#102a43;color:#fff;border-radius:8px;padding:9px 10px;display:none;white-space:pre-line}.warn{color:#854d0e;font-size:12px;line-height:1.5}",
    "</style></head><body><div class=\"app\"><h1>課程週次填寫表單</h1><div class=\"muted\" id=\"meta\">讀取中...</div><div id=\"root\"></div><div id=\"toast\"></div></div>",
    "<script>",
    "var current=null;",
    "function esc(v){return String(v||'').replace(/[&<>\"']/g,function(c){return {'&':'&amp;','<':'&lt;','>':'&gt;','\"':'&quot;',\"'\":'&#39;'}[c];});}",
    "function toast(msg){var el=document.getElementById('toast');el.textContent=msg;el.style.display='block';setTimeout(function(){el.style.display='none';},3600);}",
    "function busy(on){Array.prototype.forEach.call(document.querySelectorAll('button'),function(btn){btn.disabled=on;});}",
    "function val(id){return document.getElementById(id).value;}",
    "function payload(){return {weekId:current.weekId,topic:val('topic'),concepts:val('concepts'),activities:val('activities'),assignments:val('assignments'),nextPreview:val('nextPreview'),resources:val('resources')};}",
    "function runCreate(fn){busy(true);var r=google.script.run.withSuccessHandler(function(){busy(false);load();}).withFailureHandler(function(e){busy(false);toast(e&&e.message?e.message:String(e));});if(fn==='current')r.UCR_createCurrentWeekReport();if(fn==='next')r.UCR_createNextWeekReport();}",
    "function save(generate){busy(true);var r=google.script.run.withSuccessHandler(function(res){busy(false);toast(res.message||'已完成');load();}).withFailureHandler(function(e){busy(false);toast(e&&e.message?e.message:String(e));});if(generate){r.UCR_saveWeeklyInputFormAndGenerate(payload());}else{r.UCR_saveWeeklyInputForm(payload());}}",
    "function load(){google.script.run.withSuccessHandler(render).withFailureHandler(function(e){toast(e&&e.message?e.message:String(e));}).UCR_getWeeklyInputFormData();}",
    "function render(d){current=d;document.getElementById('meta').textContent=d.hasWeek?(d.courseName+'|'+d.weekId+'|'+d.dateRange):d.courseName+'|尚未建立週次';var h='';if(!d.hasWeek){h+='<div class=\"notice\">尚未建立本週課程重點範本。請先建立週次,再填寫內容。</div><div class=\"actions\"><button class=\"primary\" onclick=\"runCreate(\\'current\\')\">建立本週課程重點範本</button><button onclick=\"runCreate(\\'next\\')\">建立下一週課程重點範本</button></div>';document.getElementById('root').innerHTML=h;return;}if(d.warnings&&d.warnings.length){h+='<div class=\"notice\">仍需確認:'+esc(d.warnings.join('、'))+'</div>';}h+='<div class=\"panel\"><label>課程單元主題</label><textarea class=\"short\" id=\"topic\">'+esc(d.fields.topic)+'</textarea><label>本週核心概念</label><textarea id=\"concepts\">'+esc(d.fields.concepts)+'</textarea><label>課堂活動、案例、討論或實作</label><textarea id=\"activities\">'+esc(d.fields.activities)+'</textarea><label>指定閱讀、作業、測驗或繳交事項</label><textarea id=\"assignments\">'+esc(d.fields.assignments)+'</textarea><label>下週預告與預習方向</label><textarea id=\"nextPreview\">'+esc(d.fields.nextPreview)+'</textarea><label>補充資源、影片、資料集或延伸閱讀連結</label><textarea class=\"short\" id=\"resources\">'+esc(d.fields.resources)+'</textarea><div class=\"actions two\"><button onclick=\"save(false)\">儲存草稿</button><button class=\"success\" onclick=\"save(true)\">儲存並產生三端內容</button></div></div>';document.getElementById('root').innerHTML=h;}",
    "load();",
    "</script></body></html>",
  ].join("");
}

function UCR_showStudentRegisterForm() {
  UCR_runAction_("開啟學生首次登錄表單", function () {
    const html = HtmlService.createHtmlOutput(UCR_buildStudentRegisterFormHtml_()).setTitle("學生首次登錄表單");
    SpreadsheetApp.getUi().showSidebar(html);
  });
}

function UCR_openStudentsSheet() {
  UCR_runAction_("開啟學生名單", function () {
    const ss = SpreadsheetApp.getActiveSpreadsheet();
    UCR_ensureStudentsSheet_(ss);
    ss.setActiveSheet(ss.getSheetByName(UCR_CONFIG.SHEETS.STUDENTS));
  });
}

function UCR_buildStudentRegisterFormHtml_() {
  return [
    "<!doctype html>",
    "<html><head><base target=\"_top\"><style>",
    "body{margin:0;background:#f6f8fb;color:#182230;font-family:Arial,'Noto Sans TC',sans-serif;font-size:13px}.app{padding:14px}h1{font-size:18px;margin:0 0 4px;color:#102a43}.muted{color:#667085;font-size:12px;line-height:1.45}.notice{border:1px solid #bed9e6;background:#eef8fb;border-radius:8px;padding:10px;margin:10px 0;color:#174a5e;line-height:1.5}.panel{background:#fff;border:1px solid #d9e2ec;border-radius:8px;padding:12px;margin:10px 0;box-shadow:0 1px 2px rgba(16,42,67,.04)}label{display:block;font-weight:700;color:#243b53;margin:12px 0 6px}input,textarea{box-sizing:border-box;width:100%;border:1px solid #cbd5e1;border-radius:8px;padding:10px;font-size:13px;line-height:1.5;background:#fff;color:#111827}textarea{min-height:74px;resize:vertical}.check{display:flex;align-items:center;gap:8px;margin-top:12px;font-weight:700;color:#243b53}.check input{width:auto}.actions{display:grid;grid-template-columns:1fr;gap:8px;margin-top:12px}button{border:1px solid #b8c7d9;border-radius:8px;background:#243b53;color:#fff;font-weight:700;font-size:13px;padding:11px 12px;cursor:pointer}button:disabled{opacity:.55;cursor:wait}#toast{position:sticky;bottom:0;margin-top:10px;background:#102a43;color:#fff;border-radius:8px;padding:9px 10px;display:none;white-space:pre-line}.error{background:#7f1d1d}",
    "</style></head><body><div class=\"app\"><h1>學生首次登錄表單</h1><div class=\"muted\">請填寫收件 Email。系統會依訂閱狀態寄送課程週三報。</div><div class=\"notice\">必填:學號、姓名、Email。若資料有異動,再送出一次即可更新。</div><div class=\"panel\"><label>學號 *</label><input id=\"studentId\" autocomplete=\"off\"><label>姓名 *</label><input id=\"name\" autocomplete=\"name\"><label>Email *</label><input id=\"email\" type=\"email\" autocomplete=\"email\"><label>系級/班級</label><input id=\"className\"><label>備註</label><textarea id=\"note\"></textarea><label class=\"check\"><input id=\"subscribed\" type=\"checkbox\" checked>訂閱週三報 Email</label><div class=\"actions\"><button id=\"submitBtn\" onclick=\"submitForm()\">送出登錄資料</button></div></div><div id=\"toast\"></div></div>",
    "<script>",
    "function v(id){return document.getElementById(id).value.trim();}",
    "function toast(msg,isError){var el=document.getElementById('toast');el.textContent=msg;el.className=isError?'error':'';el.style.display='block';}",
    "function busy(on){document.getElementById('submitBtn').disabled=on;}",
    "function submitForm(){var payload={studentId:v('studentId'),name:v('name'),email:v('email'),className:v('className'),note:v('note'),subscribed:document.getElementById('subscribed').checked};if(!payload.studentId||!payload.name||!payload.email){toast('請填寫學號、姓名與 Email。',true);return;}if(!/^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(payload.email)){toast('Email 格式看起來不正確,請再確認。',true);return;}busy(true);google.script.run.withSuccessHandler(function(res){busy(false);toast(res&&res.message?res.message:'已完成登錄。',!(res&&res.ok));}).withFailureHandler(function(e){busy(false);toast(e&&e.message?e.message:String(e),true);}).UCR_submitStudentRegistration(payload);}",
    "</script></body></html>",
  ].join("");
}

function UCR_submitStudentRegistration(payload) {
  return UCR_runFormAction_("學生首次登錄", function () {
    const ss = SpreadsheetApp.getActiveSpreadsheet();
    UCR_ensureStudentsSheet_(ss);
    const sheet = ss.getSheetByName(UCR_CONFIG.SHEETS.STUDENTS);
    const studentId = String(payload && payload.studentId ? payload.studentId : "").trim();
    const name = String(payload && payload.name ? payload.name : "").trim();
    const email = String(payload && payload.email ? payload.email : "").trim();
    const className = String(payload && payload.className ? payload.className : "").trim();
    const note = String(payload && payload.note ? payload.note : "").trim();
    const subscribed = payload && payload.subscribed === false ? "否" : "是";
    if (!studentId || !name || !email) {
      return { ok: false, message: "請填寫學號、姓名與 Email。" };
    }
    if (!UCR_isValidEmail_(email)) {
      return { ok: false, message: "Email 格式不正確,請重新確認。" };
    }
    const now = new Date();
    const existingRow = UCR_findStudentRowById_(sheet, studentId);
    const emailRows = UCR_findStudentRowsByEmail_(sheet, email).filter(function (row) {
      if (existingRow > 0 && row === existingRow) return false;
      const existingId = String(sheet.getRange(row, 2).getValue() || "").trim();
      return existingId !== studentId;
    });
    const duplicateEmailWarning = emailRows.length > 0 ? "\n提醒:此 Email 已出現在其他學號資料中,請教師或助教人工確認是否為共用信箱或誤填。" : "";
    if (existingRow > 0) {
      const registeredAt = sheet.getRange(existingRow, 1).getValue() || now;
      sheet.getRange(existingRow, 1, 1, 8).setValues([[registeredAt, studentId, name, email, className, note, subscribed, now]]);
      UCR_refreshStudentsSummaryOnDashboard_();
      return { ok: true, message: "已更新你的登錄資料。" + duplicateEmailWarning };
    }
    sheet.appendRow([now, studentId, name, email, className, note, subscribed, now]);
    UCR_refreshStudentsSummaryOnDashboard_();
    return { ok: true, message: "已完成首次登錄,之後課程週三報會寄送到你的 Email。" + duplicateEmailWarning };
  });
}

function UCR_getSubscribedStudentRecipients_() {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  UCR_ensureStudentsSheet_(ss);
  const sheet = ss.getSheetByName(UCR_CONFIG.SHEETS.STUDENTS);
  if (!sheet || sheet.getLastRow() < 2) return [];
  return sheet.getRange(2, 1, sheet.getLastRow() - 1, 8).getValues().map(function (row) {
    return {
      studentId: String(row[1] || "").trim(),
      name: String(row[2] || "").trim(),
      email: String(row[3] || "").trim(),
      subscribed: String(row[6] || "").trim(),
    };
  }).filter(function (student) {
    return student.subscribed === "是" && student.email && UCR_isValidEmail_(student.email);
  }).map(function (student) {
    return { studentId: student.studentId, name: student.name, email: student.email };
  });
}

function UCR_getStudentsSummary_() {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  UCR_ensureStudentsSheet_(ss);
  const sheet = ss.getSheetByName(UCR_CONFIG.SHEETS.STUDENTS);
  const summary = { total: 0, subscribed: 0, validEmail: 0, invalidEmail: 0 };
  if (!sheet || sheet.getLastRow() < 2) return summary;
  sheet.getRange(2, 1, sheet.getLastRow() - 1, 8).getValues().forEach(function (row) {
    const studentId = String(row[1] || "").trim();
    const name = String(row[2] || "").trim();
    const email = String(row[3] || "").trim();
    const subscribed = String(row[6] || "").trim();
    if (!studentId && !name && !email) return;
    summary.total++;
    if (subscribed === "是") summary.subscribed++;
    if (email && UCR_isValidEmail_(email)) {
      summary.validEmail++;
    } else if (email) {
      summary.invalidEmail++;
    }
  });
  return summary;
}

function UCR_showWeeklyPublishPanel() {
  UCR_runAction_("開啟本週發布面板", function () {
    const html = HtmlService.createHtmlOutput(UCR_buildWeeklyPublishPanelHtml_()).setTitle("本週課程發布面板");
    SpreadsheetApp.getUi().showSidebar(html);
  });
}

function UCR_getWeeklyPublishPanelData() {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  UCR_ensureStructure_(ss);
  const weekId = UCR_getActiveWeekId_() || UCR_getLatestWeekId_();
  const settings = UCR_getSettings_();
  const empty = {
    hasWeek: false,
    weekId: "",
    dateRange: "",
    overallStatus: UCR_CONFIG.STATUS.NOT_CREATED,
    nextAction: "請先建立本週課程重點範本。",
    coursePlatformUrl: settings["課程平台網址"] || "",
    platformName: settings["協作平台名稱"] || "Google Chat",
    copiesReady: false,
    docReady: false,
    folderReady: false,
    links: { docUrl: "", folderUrl: "", coursePlatformUrl: settings["課程平台網址"] || "" },
    publishInfo: UCR_getPublishInfo_(""),
    copies: { platformPlain: "", platformHtml: "", email: "", lms: "", collaborationPreview: "" },
  };
  if (!weekId) return empty;
  const data = UCR_getWeeklyDataByWeekId_(weekId);
  if (!data) return empty;
  const publishInfo = UCR_getPublishInfo_(weekId);
  const copies = UCR_getThreeChannelCopiesForWeek_(weekId);
  const copiesReady = Boolean(copies.platform || copies.email || copies.lms);
  const title = UCR_buildPlatformMaterialTitle_(data, settings);
  const plain = copies.platform || UCR_buildCoursePlatformCopy_(data, settings);
  const html = UCR_buildPlatformHtml_(title, UCR_makeWeeklySummary_(data), plain, publishInfo.docUrl, data.materialsFolderUrl);
  return {
    hasWeek: true,
    weekId: weekId,
    dateRange: data.dateRange,
    overallStatus: UCR_getOverallStatus_(publishInfo, copiesReady),
    nextAction: UCR_getNextActionText_(data, publishInfo, copiesReady),
    coursePlatformUrl: settings["課程平台網址"] || "",
    platformName: settings["協作平台名稱"] || "Google Chat",
    copiesReady: copiesReady,
    docReady: Boolean(publishInfo.docUrl),
    folderReady: Boolean(data.materialsFolderUrl),
    links: { docUrl: publishInfo.docUrl || "", folderUrl: data.materialsFolderUrl || "", coursePlatformUrl: settings["課程平台網址"] || "" },
    publishInfo: publishInfo,
    copies: {
      platformPlain: plain,
      platformHtml: html,
      email: copies.email || "",
      lms: copies.lms || "",
      collaborationPreview: UCR_buildGoogleChatMessage_(weekId, data, settings, publishInfo, publishInfo.docUrl, data.materialsFolderUrl),
    },
  };
}

function UCR_buildWeeklyPublishPanelHtml_() {
  return [
    "<!doctype html>",
    "<html><head><base target=\"_top\"><style>",
    "body{margin:0;background:#f6f8fb;color:#182230;font-family:Arial,'Noto Sans TC',sans-serif;font-size:13px}.app{padding:14px}h1{font-size:18px;margin:0;color:#102a43}.top{display:flex;justify-content:space-between;gap:10px;margin-bottom:12px}.muted{color:#667085;font-size:12px;line-height:1.45}.status{border:1px solid #d6dee8;border-radius:8px;background:#fff;padding:6px 8px;font-weight:700;white-space:nowrap}.notice{border:1px solid #f6d58f;background:#fff8e5;border-radius:8px;padding:10px;margin:10px 0;color:#7a4b00;line-height:1.5}.panel{background:#fff;border:1px solid #d9e2ec;border-radius:8px;padding:12px;margin:10px 0;box-shadow:0 1px 2px rgba(16,42,67,.04)}.panel h2{font-size:15px;margin:0 0 8px;color:#102a43}.grid{display:grid;grid-template-columns:1fr 1fr;gap:8px}.field{border:1px solid #e1e8f0;background:#fbfcfe;border-radius:8px;padding:8px;min-width:0}.field label{display:block;color:#667085;font-size:11px;margin-bottom:4px}a{color:#1155cc;text-decoration:underline;word-break:break-word}textarea{box-sizing:border-box;width:100%;min-height:130px;border:1px solid #cbd5e1;border-radius:8px;padding:10px;font-size:12px;line-height:1.5;background:#fff;color:#111827;resize:vertical}.html{min-height:170px;font-family:Menlo,Consolas,monospace;font-size:11px}.actions{display:grid;grid-template-columns:1fr;gap:8px;margin-top:8px}.two{grid-template-columns:1fr 1fr}button{border:1px solid #b8c7d9;border-radius:8px;background:#fff;color:#17324d;font-weight:700;font-size:12px;padding:9px 10px;cursor:pointer}.primary{background:#243b53;border-color:#243b53;color:#fff}.success{background:#087443;border-color:#087443;color:#fff}.pill{font-size:11px;border-radius:999px;padding:4px 7px;background:#eef2f7;color:#344054}.ok{background:#dcfce7;color:#166534}.missing{background:#fff7d6;color:#854d0e}#toast{position:sticky;bottom:0;margin-top:10px;background:#102a43;color:#fff;border-radius:8px;padding:9px 10px;display:none}",
    "</style></head><body><div class=\"app\"><div class=\"top\"><div><h1>本週課程發布面板</h1><div class=\"muted\" id=\"meta\">讀取中...</div></div><div class=\"status\" id=\"status\">...</div></div><div id=\"root\"></div><div id=\"toast\"></div></div>",
    "<script>",
    "function esc(v){return String(v||'').replace(/[&<>\"']/g,function(c){return {'&':'&amp;','<':'&lt;','>':'&gt;','\"':'&quot;',\"'\":'&#39;'}[c];});}",
    "function link(label,url){return url?'<a href=\"'+esc(url)+'\" target=\"_blank\">'+esc(label)+'</a>':'<span class=\"muted\">尚未建立</span>';}",
    "function pill(ok,text){return '<span class=\"pill '+(ok?'ok':'missing')+'\">'+esc(text)+'</span>';}",
    "function toast(msg){var el=document.getElementById('toast');el.textContent=msg;el.style.display='block';setTimeout(function(){el.style.display='none';},2600);}",
    "function copyFrom(id,label){var el=document.getElementById(id);el.focus();el.select();if(navigator.clipboard){navigator.clipboard.writeText(el.value).then(function(){toast(label+'已複製。');});}else{document.execCommand('copy');toast(label+'已選取。');}}",
    "function run(fn){var r=google.script.run.withSuccessHandler(load).withFailureHandler(function(e){toast(e&&e.message?e.message:String(e));});if(fn==='genCopies')r.UCR_generateThreeChannelCopies();if(fn==='genDoc')r.UCR_generateGoogleDocForActiveWeek();if(fn==='folder')r.UCR_createMaterialsFolderForActiveWeek();if(fn==='platformDone')r.UCR_markCoursePlatformPublished();if(fn==='emailDone')r.UCR_markEmailPublished();if(fn==='lmsDone')r.UCR_markLmsPublished();if(fn==='collab')r.UCR_publishToCollaborationPlatform();if(fn==='current')r.UCR_createCurrentWeekReport();if(fn==='next')r.UCR_createNextWeekReport();}",
    "function load(){google.script.run.withSuccessHandler(render).withFailureHandler(function(e){toast(e&&e.message?e.message:String(e));}).UCR_getWeeklyPublishPanelData();}",
    "function render(d){document.getElementById('meta').textContent=d.hasWeek?(d.weekId+'|'+d.dateRange):'尚未建立週次';document.getElementById('status').textContent=d.overallStatus;var h='';if(!d.hasWeek){h+='<div class=\"notice\">尚未建立本週課程重點範本。</div><div class=\"actions\"><button class=\"primary\" onclick=\"run(\\'current\\')\">建立本週課程重點範本</button><button onclick=\"run(\\'next\\')\">建立下一週課程重點範本</button></div>';document.getElementById('root').innerHTML=h;return;}h+='<div class=\"notice\"><strong>下一步:</strong>'+esc(d.nextAction)+'</div>';h+='<div class=\"grid\"><div class=\"field\"><label>Google Docs</label>'+link('開啟 Docs',d.links.docUrl)+'</div><div class=\"field\"><label>素材資料夾</label>'+link('開啟素材資料夾',d.links.folderUrl)+'</div><div class=\"field\"><label>課程平台</label>'+link('開啟課程平台',d.links.coursePlatformUrl)+'</div><div class=\"field\"><label>內容狀態</label>'+(d.copiesReady?pill(true,'已產生三端內容'):pill(false,'尚未產生三端內容'))+'</div></div>';if(!d.copiesReady){h+='<div class=\"notice\">尚未產生三端內容。</div><button class=\"primary\" onclick=\"run(\\'genCopies\\')\">產生三端內容</button>';}if(!d.docReady||!d.folderReady){h+='<div class=\"panel\"><h2>發布前準備</h2><div class=\"actions two\">';if(!d.docReady)h+='<button onclick=\"run(\\'genDoc\\')\">建立 Google Docs</button>';if(!d.folderReady)h+='<button onclick=\"run(\\'folder\\')\">建立素材資料夾</button>';h+='</div></div>';}h+=sectionPlatform(d)+sectionEmail(d)+sectionLms(d)+sectionCollab(d);document.getElementById('root').innerHTML=h;}",
    "function sectionPlatform(d){return '<div class=\"panel\"><h2>課程平台公告 '+pill(d.copiesReady,'純文字 / HTML')+'</h2><textarea id=\"platformPlain\">'+esc(d.copies.platformPlain)+'</textarea><textarea class=\"html\" id=\"platformHtml\">'+esc(d.copies.platformHtml)+'</textarea><div class=\"actions two\"><button onclick=\"copyFrom(\\'platformPlain\\',\\'課程平台純文字\\')\">複製純文字</button><button onclick=\"copyFrom(\\'platformHtml\\',\\'課程平台HTML\\')\">複製HTML</button><button class=\"success\" onclick=\"run(\\'platformDone\\')\">標記課程平台已發布</button></div><div class=\"muted\">目前狀態:'+esc(d.publishInfo.platformStatus)+'</div></div>';}",
    "function sectionEmail(d){return '<div class=\"panel\"><h2>Email提醒 '+pill(Boolean(d.copies.email),'可複製')+'</h2><textarea id=\"emailCopy\">'+esc(d.copies.email)+'</textarea><div class=\"actions two\"><button onclick=\"copyFrom(\\'emailCopy\\',\\'Email文案\\')\">複製Email文案</button><button class=\"success\" onclick=\"run(\\'emailDone\\')\">標記Email已發布</button></div><div class=\"muted\">目前狀態:'+esc(d.publishInfo.emailStatus)+'</div></div>';}",
    "function sectionLms(d){return '<div class=\"panel\"><h2>LMS/社群 '+pill(Boolean(d.copies.lms),'可複製')+'</h2><textarea id=\"lmsCopy\">'+esc(d.copies.lms)+'</textarea><div class=\"actions two\"><button onclick=\"copyFrom(\\'lmsCopy\\',\\'LMS社群文案\\')\">複製LMS/社群文案</button><button class=\"success\" onclick=\"run(\\'lmsDone\\')\">標記LMS/社群已發布</button></div><div class=\"muted\">目前狀態:'+esc(d.publishInfo.lmsStatus)+'</div></div>';}",
    "function sectionCollab(d){return '<div class=\"panel\"><h2>協作平台 '+pill(d.publishInfo.collaborationStatus&&d.publishInfo.collaborationStatus.indexOf('已發布')===0,d.platformName)+'</h2><textarea id=\"collabPreview\">'+esc(d.copies.collaborationPreview)+'</textarea><div class=\"actions\"><button class=\"primary\" onclick=\"run(\\'collab\\')\">發布到協作平台</button></div><div class=\"muted\">目前狀態:'+esc(d.publishInfo.collaborationStatus)+'</div></div>';}",
    "load();",
    "</script></body></html>",
  ].join("");
}

function UCR_markCoursePlatformPublished() {
  UCR_runAction_("標記課程平台已發布", function () {
    UCR_markPublished_("課程平台");
  });
}

function UCR_sendWeeklyEmailToStudents() {
  UCR_runAction_("寄送本週週三報給學生", function () {
    const weekId = UCR_getActiveWeekId_() || UCR_getLatestWeekId_();
    if (!weekId) {
      UCR_alert_("目前沒有週次ID,請先建立本週課程重點範本。");
      return;
    }
    const copies = UCR_getThreeChannelCopiesForWeek_(weekId);
    if (!copies.email) {
      UCR_alert_("尚未產生 Email短提醒版。請先執行「產生三端內容」。");
      return;
    }
    const recipients = UCR_getSubscribedStudentRecipients_();
    if (recipients.length === 0) {
      UCR_alert_("沒有有效收件者。請先請學生完成首次登錄,並確認已訂閱週三報且 Email 格式有效。");
      return;
    }
    // Google Apps Script / Gmail / MailApp 有每日寄送配額限制,實際上限依帳號類型不同。
    // 研習現場避免無提示大量寄送;超過 80 人時先請教師分批或改由課程平台公告。
    if (recipients.length > 80) {
      UCR_alert_("目前有效收件者為 " + recipients.length + " 人,超過 80 人。為避免 Gmail / MailApp 配額與大量寄送風險,請先分批寄送或改由課程平台公告。");
      return;
    }
    const remainingQuota = MailApp.getRemainingDailyQuota();
    if (remainingQuota < recipients.length) {
      UCR_alert_("今日 MailApp 剩餘寄送配額不足。\n有效收件者:" + recipients.length + " 人\n剩餘配額:" + remainingQuota + "\n請改日再寄送或改用課程平台公告。");
      return;
    }
    if (!UCR_confirm_("確認寄送", "即將寄送本週週三報給 " + recipients.length + " 位已訂閱學生,是否繼續?")) {
      UCR_alert_("已取消寄送。");
      return;
    }
    const settings = UCR_getSettings_();
    const courseName = settings["課程名稱"] || "課程";
    const instructor = settings["授課教師"] || "授課教師";
    const platformUrl = settings["課程平台網址"] || "";
    const subject = "[" + courseName + "] 週三報|" + weekId;
    const body = UCR_buildStudentWeeklyEmailBody_(copies.email, courseName, instructor, platformUrl);
    const to = UCR_getMailSenderAddress_();
    if (!to) {
      UCR_alert_("無法取得寄送者 Email。請確認目前帳號可使用 Gmail / MailApp,或改用手動複製 Email 文案。");
      return;
    }
    MailApp.sendEmail({
      to: to,
      bcc: recipients.map(function (student) {
        return student.email;
      }).join(","),
      subject: subject,
      body: body,
      name: courseName + " 週三報",
    });
    UCR_updatePublishLogEmailStatus_(weekId, "已寄送給學生", recipients.length);
    UCR_refreshDashboardForWeek_(weekId);
    UCR_updateWeeklyIndex_();
    UCR_alert_("已寄送本週週三報給學生。\n有效收件者:" + recipients.length + " 人");
  });
}

function UCR_markEmailPublished() {
  UCR_runAction_("標記 Email 已發布", function () {
    UCR_markPublished_("Email");
  });
}

function UCR_markLmsPublished() {
  UCR_runAction_("標記 LMS/社群 已發布", function () {
    UCR_markPublished_("LMS/社群");
  });
}

function UCR_markPublished_(channelName) {
  const weekId = UCR_getActiveWeekId_();
  if (!weekId) {
    UCR_alert_("目前沒有週次ID,請先建立本週課程重點範本。");
    return;
  }
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const sheet = ss.getSheetByName(UCR_CONFIG.SHEETS.PUBLISH_LOG);
  UCR_ensurePublishLogRow_(weekId);
  const row = UCR_findRowByWeekId_(sheet, weekId);
  const nowText = Utilities.formatDate(new Date(), UCR_CONFIG.TIMEZONE, "yyyy/MM/dd HH:mm");
  let col = 0;
  if (channelName === "課程平台") col = 2;
  if (channelName === "Email") col = 3;
  if (channelName === "LMS/社群") col = 4;
  if (!col) {
    UCR_alert_("未知的發布管道:" + channelName);
    return;
  }
  sheet.getRange(row, col).setValue("已發布 " + nowText);
  sheet.getRange(row, 8).setValue(new Date());
  UCR_refreshDashboardForWeek_(weekId);
  UCR_updateWeeklyIndex_();
  UCR_alert_(channelName + " 已標記發布。");
}

function UCR_ensurePublishLogRow_(weekId) {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const sheet = ss.getSheetByName(UCR_CONFIG.SHEETS.PUBLISH_LOG);
  if (!sheet) return;
  if (UCR_findRowByWeekId_(sheet, weekId) > 0) return;
  sheet.appendRow([weekId, "尚未發布", "尚未發布", "尚未發布", "尚未發布", "", "", new Date()]);
}

function UCR_updatePublishLogDocUrl_(weekId, docUrl) {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const sheet = ss.getSheetByName(UCR_CONFIG.SHEETS.PUBLISH_LOG);
  if (!sheet) {
    UCR_alert_("找不到「03_發布紀錄」,請先執行「初始化平台」。");
    return;
  }
  UCR_ensurePublishLogRow_(weekId);
  const row = UCR_findRowByWeekId_(sheet, weekId);
  sheet.getRange(row, 6).setValue(docUrl);
  sheet.getRange(row, 8).setValue(new Date());
}

function UCR_updatePublishLogCollaborationStatus_(weekId, platformName) {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const sheet = ss.getSheetByName(UCR_CONFIG.SHEETS.PUBLISH_LOG);
  UCR_ensurePublishLogRow_(weekId);
  const row = UCR_findRowByWeekId_(sheet, weekId);
  const nowText = Utilities.formatDate(new Date(), UCR_CONFIG.TIMEZONE, "yyyy/MM/dd HH:mm");
  sheet.getRange(row, 5).setValue("已發布到 " + platformName + " " + nowText);
  sheet.getRange(row, 8).setValue(new Date());
}

function UCR_updatePublishLogEmailStatus_(weekId, statusPrefix, recipientCount) {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const sheet = ss.getSheetByName(UCR_CONFIG.SHEETS.PUBLISH_LOG);
  UCR_ensurePublishLogRow_(weekId);
  const row = UCR_findRowByWeekId_(sheet, weekId);
  const now = new Date();
  const nowText = Utilities.formatDate(now, UCR_CONFIG.TIMEZONE, "yyyy/MM/dd HH:mm");
  sheet.getRange(row, 3).setValue(statusPrefix + " " + nowText + ",有效收件者 " + recipientCount + " 人");
  sheet.getRange(row, 8).setValue(now);
}

function UCR_updateWeeklyIndex() {
  UCR_runAction_("更新歷週索引", function () {
    UCR_updateWeeklyIndex_();
    UCR_alert_("歷週索引已更新。");
  });
}

function UCR_updateWeeklyIndex_() {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const indexSheet = ss.getSheetByName(UCR_CONFIG.SHEETS.INDEX);
  const weeklySheet = ss.getSheetByName(UCR_CONFIG.SHEETS.WEEKLY_DATA);
  const logSheet = ss.getSheetByName(UCR_CONFIG.SHEETS.PUBLISH_LOG);
  if (!indexSheet || !weeklySheet || !logSheet) return;
  indexSheet.getRange(2, 1, Math.max(indexSheet.getMaxRows() - 1, 1), 8).clearContent().clearFormat();
  UCR_styleHeader_(indexSheet, 8);
  if (weeklySheet.getLastRow() < 2) return;
  const settings = UCR_getSettings_();
  const platformUrl = settings["課程平台網址"] || "";
  const rows = [];
  const links = [];
  weeklySheet.getRange(2, 1, weeklySheet.getLastRow() - 1, 11).getValues().forEach(function (row) {
    const weekId = row[0];
    if (!weekId) return;
    const data = UCR_rowToWeeklyData_(row);
    const logRow = UCR_findRowByWeekId_(logSheet, weekId);
    let docUrl = "";
    let publishStatus = "尚未發布";
    let lastUpdated = data.updatedAt || "";
    if (logRow > 0) {
      const log = logSheet.getRange(logRow, 1, 1, 8).getValues()[0];
      docUrl = log[5];
      publishStatus = UCR_buildPublishSummary_(log[1], log[2], log[3], log[4]);
      lastUpdated = log[7] || lastUpdated;
    }
    rows.push([
      weekId,
      data.dateRange,
      UCR_makeWeeklySummary_(data),
      docUrl ? "開啟 Docs" : "尚未建立",
      data.materialsFolderUrl ? "開啟素材資料夾" : "尚未建立",
      platformUrl ? "開啟課程平台" : "尚未設定",
      publishStatus,
      lastUpdated,
    ]);
    links.push({ docUrl: docUrl, folderUrl: data.materialsFolderUrl, platformUrl: platformUrl });
  });
  if (rows.length > 0) {
    indexSheet.getRange(2, 1, rows.length, 8).setValues(rows);
    links.forEach(function (item, i) {
      const row = i + 2;
      UCR_setLinkCell_(indexSheet, row, 4, "開啟 Docs", item.docUrl, "尚未建立");
      UCR_setLinkCell_(indexSheet, row, 5, "開啟素材資料夾", item.folderUrl, "尚未建立");
      UCR_setLinkCell_(indexSheet, row, 6, "開啟課程平台", item.platformUrl, "尚未設定");
    });
  }
}

function UCR_refreshDashboardForWeek_(weekId) {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const dashboard = ss.getSheetByName(UCR_CONFIG.SHEETS.DASHBOARD);
  if (!dashboard) return;
  UCR_updateDashboardStudentsSummary_(dashboard);
  const data = weekId ? UCR_getWeeklyDataByWeekId_(weekId) : null;
  const settings = UCR_getSettings_();
  const platformUrl = settings["課程平台網址"] || "";
  const publishInfo = UCR_getPublishInfo_(weekId);
  const hasCopies = UCR_hasThreeChannelCopies_(weekId);
  if (!data) {
    dashboard.getRange("B4:B5").clearContent();
    UCR_setStatusCell_(dashboard, "E4", UCR_CONFIG.STATUS.NOT_CREATED);
    UCR_updateDashboardFlowSteps_(dashboard, null, publishInfo, hasCopies);
    return;
  }
  dashboard.getRange("B4").setValue(data.weekId);
  dashboard.getRange("B5").setValue(data.dateRange);
  UCR_setStatusCell_(dashboard, "E4", UCR_getOverallStatus_(publishInfo, hasCopies));
  UCR_setLinkCell_(dashboard, 5, 5, "開啟 Google Docs", publishInfo.docUrl, "尚未建立");
  UCR_setLinkCell_(dashboard, 6, 2, "開啟課程平台", platformUrl, "尚未設定");
  UCR_setLinkCell_(dashboard, 6, 5, "開啟素材資料夾", data.materialsFolderUrl, "尚未建立");
  dashboard.getRange("B9").setValue(UCR_CONFIG.STATUS.DRAFT);
  dashboard.getRange("D9").setValue(hasCopies ? UCR_CONFIG.STATUS.COPY_READY : "⚪ 尚未產生");
  dashboard.getRange("F9").setValue(publishInfo.docUrl ? UCR_CONFIG.STATUS.DOC_READY : "⚪ 尚未產生");
  dashboard.getRange("B10").setValue(UCR_isPublished_(publishInfo.platformStatus) && UCR_isPublished_(publishInfo.emailStatus) && UCR_isPublished_(publishInfo.lmsStatus) ? UCR_CONFIG.STATUS.COMPLETED : "⚪ 尚未完成");
  dashboard.getRange("D10").setValue(UCR_isPublished_(publishInfo.collaborationStatus) ? UCR_CONFIG.STATUS.COLLAB_READY : "⚪ 尚未發布");
  dashboard.getRange("B13").setValue(publishInfo.platformStatus || "尚未發布");
  dashboard.getRange("B14").setValue(publishInfo.emailStatus || "尚未發布");
  dashboard.getRange("B15").setValue(publishInfo.lmsStatus || "尚未發布");
  dashboard.getRange("B16").setValue(publishInfo.collaborationStatus || "尚未發布");
  UCR_updateDashboardFlowSteps_(dashboard, data, publishInfo, hasCopies);
}

function UCR_updateDashboardFlowSteps_(dashboard, data, publishInfo, hasCopies) {
  const hasWeek = Boolean(data);
  const fieldsDone = hasWeek && UCR_getIncompleteFieldWarnings_(data).length === 0;
  const docAndFolderDone = hasWeek && Boolean(publishInfo.docUrl) && Boolean(data.materialsFolderUrl);
  const threeDone = UCR_isPublished_(publishInfo.platformStatus) && UCR_isPublished_(publishInfo.emailStatus) && UCR_isPublished_(publishInfo.lmsStatus);
  const collabDone = UCR_isPublished_(publishInfo.collaborationStatus);
  dashboard.getRange(19, 3, 6, 1).setValues([hasWeek, fieldsDone, hasCopies, docAndFolderDone, threeDone, collabDone].map(function (done) {
    return [done ? "✅ 已完成" : "⚪ 待處理"];
  }));
  dashboard.getRange("E19").setValue(hasWeek ? UCR_getNextActionText_(data, publishInfo, hasCopies) : "請先建立本週課程重點範本");
}

function UCR_getNextActionText_(data, publishInfo, hasCopies) {
  if (!data) return "請先建立本週課程重點範本。";
  if (UCR_getIncompleteFieldWarnings_(data).length > 0) return "請補齊 01_課程週次資料中仍是提示文字的欄位。";
  if (!hasCopies) return "請產生三端內容。";
  if (!publishInfo.docUrl) return "請建立 Google Docs 課程重點。";
  if (!data.materialsFolderUrl) return "請建立本週課程素材資料夾。";
  if (!UCR_isPublished_(publishInfo.platformStatus)) return "請複製課程平台素材並標記課程平台已發布。";
  if (!UCR_isPublished_(publishInfo.emailStatus)) return "請複製 Email 文案並標記 Email 已發布。";
  if (!UCR_isPublished_(publishInfo.lmsStatus)) return "請複製 LMS/社群文案並標記 LMS/社群已發布。";
  if (!UCR_isPublished_(publishInfo.collaborationStatus)) return "可發布到協作平台,同步課程助教或共備團隊。";
  return "本週課程內容重點流程已完成。";
}

function UCR_getOverallStatus_(publishInfo, hasCopies) {
  if (UCR_isPublished_(publishInfo.platformStatus) && UCR_isPublished_(publishInfo.emailStatus) && UCR_isPublished_(publishInfo.lmsStatus)) return UCR_CONFIG.STATUS.COMPLETED;
  if (UCR_isPublished_(publishInfo.collaborationStatus)) return UCR_CONFIG.STATUS.COLLAB_READY;
  if (publishInfo.docUrl) return UCR_CONFIG.STATUS.DOC_READY;
  if (hasCopies) return UCR_CONFIG.STATUS.COPY_READY;
  return UCR_CONFIG.STATUS.DRAFT;
}

function UCR_getThreeChannelCopiesForWeek_(weekId) {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const sheet = ss.getSheetByName(UCR_CONFIG.SHEETS.COPY_OUTPUT);
  if (!sheet) return { platform: "", email: "", lms: "" };
  const row = UCR_findRowByWeekId_(sheet, weekId);
  if (row <= 0) return { platform: "", email: "", lms: "" };
  const values = sheet.getRange(row, 1, 1, 6).getValues()[0];
  return { platform: values[1] || "", email: values[2] || "", lms: values[3] || "" };
}

function UCR_getPublishInfo_(weekId) {
  const empty = { platformStatus: "尚未發布", emailStatus: "尚未發布", lmsStatus: "尚未發布", collaborationStatus: "尚未發布", docUrl: "" };
  if (!weekId) return empty;
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const sheet = ss.getSheetByName(UCR_CONFIG.SHEETS.PUBLISH_LOG);
  if (!sheet) return empty;
  const row = UCR_findRowByWeekId_(sheet, weekId);
  if (row <= 0) return empty;
  const values = sheet.getRange(row, 1, 1, 8).getValues()[0];
  return {
    platformStatus: values[1] || "尚未發布",
    emailStatus: values[2] || "尚未發布",
    lmsStatus: values[3] || "尚未發布",
    collaborationStatus: values[4] || "尚未發布",
    docUrl: values[5] || "",
  };
}

function UCR_hasThreeChannelCopies_(weekId) {
  const copies = UCR_getThreeChannelCopiesForWeek_(weekId);
  return Boolean(copies.platform || copies.email || copies.lms);
}

function UCR_refreshStudentsSummaryOnDashboard_() {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const dashboard = ss.getSheetByName(UCR_CONFIG.SHEETS.DASHBOARD);
  if (!dashboard) return;
  UCR_updateDashboardStudentsSummary_(dashboard);
}

function UCR_updateDashboardStudentsSummary_(dashboard) {
  const summary = UCR_getStudentsSummary_();
  dashboard.getRange("E14").setValue(summary.total);
  dashboard.getRange("E15").setValue(summary.subscribed);
  dashboard.getRange("E16").setValue(summary.validEmail);
}

function UCR_getActiveWeekId_() {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const dashboard = ss.getSheetByName(UCR_CONFIG.SHEETS.DASHBOARD);
  if (!dashboard) return "";
  return dashboard.getRange("B4").getValue();
}

function UCR_getLatestWeekId_() {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const sheet = ss.getSheetByName(UCR_CONFIG.SHEETS.WEEKLY_DATA);
  if (!sheet || sheet.getLastRow() < 2) return "";
  return sheet.getRange(sheet.getLastRow(), 1).getValue();
}

function UCR_getWeeklyDataByWeekId_(weekId) {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const sheet = ss.getSheetByName(UCR_CONFIG.SHEETS.WEEKLY_DATA);
  if (!sheet) return null;
  const row = UCR_findRowByWeekId_(sheet, weekId);
  if (row <= 0) return null;
  return UCR_rowToWeeklyData_(sheet.getRange(row, 1, 1, 11).getValues()[0]);
}

function UCR_rowToWeeklyData_(values) {
  return {
    weekId: values[0],
    dateRange: values[1],
    topic: values[2],
    concepts: values[3],
    activities: values[4],
    assignments: values[5],
    nextPreview: values[6],
    resources: values[7],
    materialsFolderUrl: values[8],
    createdAt: values[9],
    updatedAt: values[10],
  };
}

function UCR_getSettings_() {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const sheet = ss.getSheetByName(UCR_CONFIG.SHEETS.SETTINGS);
  const settings = {};
  if (!sheet || sheet.getLastRow() < 2) return settings;
  sheet.getRange(2, 1, sheet.getLastRow() - 1, 2).getValues().forEach(function (row) {
    if (row[0]) settings[row[0]] = row[1];
  });
  return settings;
}

function UCR_findSettingsRow_(settingName) {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const sheet = ss.getSheetByName(UCR_CONFIG.SHEETS.SETTINGS);
  if (!sheet) return -1;
  const values = sheet.getRange(1, 1, sheet.getLastRow(), 1).getValues();
  for (let i = 0; i < values.length; i++) {
    if (values[i][0] === settingName) return i + 1;
  }
  return -1;
}

function UCR_findRowByWeekId_(sheet, weekId) {
  if (!sheet || !weekId || sheet.getLastRow() < 2) return -1;
  const values = sheet.getRange(2, 1, sheet.getLastRow() - 1, 1).getValues();
  for (let i = 0; i < values.length; i++) {
    if (values[i][0] === weekId) return i + 2;
  }
  return -1;
}

function UCR_findStudentRowById_(sheet, studentId) {
  if (!sheet || !studentId || sheet.getLastRow() < 2) return -1;
  const values = sheet.getRange(2, 2, sheet.getLastRow() - 1, 1).getValues();
  for (let i = 0; i < values.length; i++) {
    if (String(values[i][0] || "").trim() === studentId) return i + 2;
  }
  return -1;
}

function UCR_findStudentRowsByEmail_(sheet, email) {
  if (!sheet || !email || sheet.getLastRow() < 2) return [];
  const normalized = String(email || "").trim().toLowerCase();
  const values = sheet.getRange(2, 4, sheet.getLastRow() - 1, 1).getValues();
  const rows = [];
  for (let i = 0; i < values.length; i++) {
    if (String(values[i][0] || "").trim().toLowerCase() === normalized) rows.push(i + 2);
  }
  return rows;
}

function UCR_getIncompleteFieldWarnings_(data) {
  const fields = [
    ["課程單元主題", "topic", data.topic],
    ["本週核心概念", "concepts", data.concepts],
    ["課堂活動與討論", "activities", data.activities],
    ["指定閱讀與作業", "assignments", data.assignments],
    ["下週預告", "nextPreview", data.nextPreview],
    ["補充資源", "resources", data.resources],
  ];
  return fields.filter(function (field) {
    return UCR_isPlaceholderOrBlank_(field[1], field[2]);
  }).map(function (field) {
    return field[0];
  });
}

function UCR_isPlaceholderOrBlank_(key, value) {
  const text = String(value || "").trim();
  const placeholder = String(UCR_CONFIG.PLACEHOLDERS[key] || "").trim();
  return text === "" || text === placeholder;
}

function UCR_makeWeeklySummary_(data) {
  const source = [
    UCR_stripPlaceholder_(data.topic, "topic"),
    UCR_stripPlaceholder_(data.concepts, "concepts"),
    UCR_stripPlaceholder_(data.assignments, "assignments"),
  ].filter(function (text) {
    return text !== "";
  }).join(" / ");
  return UCR_truncateText_(source || "尚未填寫摘要", 180);
}

function UCR_buildPublishSummary_(platformStatus, emailStatus, lmsStatus, collaborationStatus) {
  return [
    "課程平台:" + (platformStatus || "尚未發布"),
    "Email:" + (emailStatus || "尚未發布"),
    "LMS/社群:" + (lmsStatus || "尚未發布"),
    "協作:" + (collaborationStatus || "尚未發布"),
  ].join("\n");
}

function UCR_buildStudentWeeklyEmailBody_(emailCopy, courseName, instructor, platformUrl) {
  const body = UCR_stripEmailSubjectLine_(emailCopy);
  return [
    body,
    "",
    "——",
    "課程名稱:" + (courseName || "課程"),
    "授課教師:" + (instructor || "授課教師"),
    platformUrl ? "課程平台:" + platformUrl : "",
  ].filter(function (line) {
    return line !== "";
  }).join("\n");
}

function UCR_stripEmailSubjectLine_(emailCopy) {
  const lines = String(emailCopy || "").split("\n");
  if (lines.length > 0 && /^主旨[::]/.test(String(lines[0] || "").trim())) {
    return lines.slice(1).join("\n").replace(/^\s+/, "");
  }
  return String(emailCopy || "").trim();
}

function UCR_getMailSenderAddress_() {
  const activeEmail = Session.getActiveUser().getEmail();
  if (activeEmail) return activeEmail;
  const effectiveEmail = Session.getEffectiveUser().getEmail();
  return effectiveEmail || "";
}

function UCR_getOrCreateSheet_(ss, sheetName) {
  let sheet = ss.getSheetByName(sheetName);
  if (!sheet) sheet = ss.insertSheet(sheetName);
  return sheet;
}

function UCR_styleHeader_(sheet, colCount) {
  sheet.getRange(1, 1, 1, colCount).setFontWeight("bold").setBackground("#d9eaf7").setHorizontalAlignment("center");
}

function UCR_setStatusCell_(sheet, a1Notation, status) {
  const range = sheet.getRange(a1Notation);
  range.setValue(status).setFontWeight("bold").setHorizontalAlignment("center");
  const colorMap = {};
  colorMap[UCR_CONFIG.STATUS.NOT_CREATED] = "#f3f4f6";
  colorMap[UCR_CONFIG.STATUS.DRAFT] = "#fff7d6";
  colorMap[UCR_CONFIG.STATUS.COPY_READY] = "#dcfce7";
  colorMap[UCR_CONFIG.STATUS.DOC_READY] = "#dbeafe";
  colorMap[UCR_CONFIG.STATUS.COLLAB_READY] = "#ede9fe";
  colorMap[UCR_CONFIG.STATUS.COMPLETED] = "#d1fae5";
  range.setBackground(colorMap[status] || "#f3f4f6");
}

function UCR_setLinkCell_(sheet, row, col, label, url, emptyText) {
  const range = sheet.getRange(row, col);
  range.clearContent();
  if (!url) {
    range.setValue(emptyText || "尚未設定").setFontColor("#6b7280").setFontLine("none");
    return;
  }
  const richText = SpreadsheetApp.newRichTextValue().setText(label).setLinkUrl(url).build();
  range.setRichTextValue(richText).setFontColor("#1155cc").setFontLine("underline");
}

function UCR_setParagraphTextColor_(paragraph, color) {
  paragraph.editAsText().setForegroundColor(color);
  return paragraph;
}

function UCR_normalizeText_(text) {
  if (!text) return "尚未填寫。";
  return String(text).trim();
}

function UCR_compactText_(text) {
  if (!text) return "尚未填寫。";
  return String(text).split("\n").map(function (line) {
    return line.trim();
  }).filter(function (line) {
    return line !== "";
  }).join("\n");
}

function UCR_stripPlaceholder_(text, key) {
  if (UCR_isPlaceholderOrBlank_(key, text)) return "";
  return UCR_compactText_(text);
}

function UCR_truncateText_(text, maxLength) {
  const normalized = String(text || "").replace(/\s+/g, " ").trim();
  if (normalized.length <= maxLength) return normalized;
  return normalized.slice(0, maxLength - 1) + "…";
}

function UCR_escapeHtml_(value) {
  return String(value || "")
    .replace(/&/g, "&amp;")
    .replace(/</g, "&lt;")
    .replace(/>/g, "&gt;")
    .replace(/"/g, "&quot;")
    .replace(/'/g, "&#39;");
}

function UCR_isPublished_(status) {
  const text = String(status || "");
  return text.indexOf("已發布") === 0 || text.indexOf("已寄送") === 0;
}

function UCR_isValidEmail_(email) {
  return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(String(email || "").trim());
}

function UCR_alert_(message) {
  SpreadsheetApp.getUi().alert(message);
}

function UCR_confirm_(title, message) {
  const ui = SpreadsheetApp.getUi();
  return ui.alert(title, message, ui.ButtonSet.YES_NO) === ui.Button.YES;
}

function UCR_runAction_(actionName, callback) {
  try {
    UCR_ensureStructure_(SpreadsheetApp.getActiveSpreadsheet());
    callback();
  } catch (e) {
    Logger.log(e && e.stack ? e.stack : e);
    UCR_alert_(actionName + "失敗。\n\n請確認已完成初始化,且已授權 Drive / Docs / Spreadsheet 權限。\n\n錯誤訊息:" + (e && e.message ? e.message : e));
  }
}
提醒:請完整複製,不要只複製其中一段。若 Apps Script 要求授權,請依畫面完成授權流程。
版本一致性提醒:本頁內嵌的 GAS 程式碼應與專案中的 .gs 原始檔保持一致。若日後修改 .gs,請同步更新講義中的一鍵複製區塊,避免研習現場使用到舊版本。

五、GAS 實作設計

實作重點是讓每位學員建立自己的課程週報平台,而不是逐行背 JavaScript。

系統架構

04_設定
    ↓
01_課程週次資料
    ↓
UCR_generateThreeChannelCopies()
    ↓
02_三端內容輸出
    ↓
Google Docs|課程平台素材|Sidebar 發布面板
    ↓
03_發布紀錄
    ↓
05_歷週索引

07_學生名單
    ↓
學生首次登錄表單|週三報 Email 收件者名單
    ↓
UCR_sendWeeklyEmailToStudents()

工作表角色

工作表角色對使用者的意義
00_課程中控台操作入口與狀態摘要看本週進度、下一步、發布狀態與學生名單摘要
01_課程週次資料原始資料庫每週只填一次課程重點、活動、學生表現與下週提醒
02_三端內容輸出轉換後文案取得學生學習報、課程紀錄報、成果亮點報與 Email 短提醒版
03_發布紀錄流程追蹤知道哪些版本已發布、寄送到哪裡、何時由誰處理
04_設定系統參數課程名稱、教師、平台連結、Email 寄送資訊
05_歷週索引長期累積期末可回顧、整理成果與引用教學歷程
06_課程平台發布素材對外發布材料可貼到 LMS、Google 協作平台或課程網站
07_學生名單首次登錄與收件者名單記錄學號、姓名、Email、系級/班級、訂閱狀態與最後更新時間

實作步驟

  1. 建立 Google Sheet,命名為「一週三報_課程內容重點_自己的課程名稱」。
  2. 開啟「擴充功能 → Apps Script」。
  3. 貼上 一週三報_大學課程內容重點.gs
  4. 儲存,回到 Sheet 重新整理。
  5. 執行「一週三報|課程內容重點 → 1. 初始化課程平台」,確認產生 00_課程中控台07_學生名單
  6. 到「04_設定」填入課程名稱、授課教師、系所/單位、課程平台網址。
  7. 需要測試 Email 收件者時,執行「開啟學生首次登錄表單」,先登錄一筆自己的測試信箱;也可直接開啟「07_學生名單」檢查資料。
  8. 執行「建立本週課程週報」。
  9. 到「01_課程週次資料」填入課程內容重點。
  10. 執行「產生一週三報」。
  11. 執行「產生 Google Docs 課程週報」與「建立課程照片/附件資料夾」。
  12. 開啟「本週課程發布面板」複製三報;若要測試寄送,執行「寄送本週週三報給學生」,確認收件者數量後再寄出。

學生首次登錄與週三報 Email 收件者

新版 GAS 增加 07_學生名單,讓課程可以建立可維護的週三報 Email 收件者名單。這不是要取代 LMS 或課程群組,而是讓教師在需要 Email 推播時,有一份可檢查、可更新、可取消訂閱的名單。

功能用途現場提醒
開啟學生首次登錄表單讓學生填寫學號、姓名、Email、系級/班級與是否訂閱週三報。研習現場先用自己的測試信箱,不要直接蒐集正式學生資料。
07_學生名單保存學生登錄資料、訂閱狀態與最後更新時間;同一學號再次登錄會更新資料。Email 欄位會做基本格式檢查,訂閱欄位為「是」才會列入寄送。
寄送本週週三報給學生讀取本週 Email短提醒版,以 BCC 寄給已訂閱且 Email 有效的學生。寄送前會顯示收件者數量確認;超過 80 人會提醒分批,避免配額與誤寄風險。
中控台學生名單摘要顯示總登錄人數、訂閱人數與有效 Email 人數。用來確認名單是否足以測試寄送,不作為成績或出缺席管理。
Email 功能的研習操作原則:先完成「一筆課程資料 → 三報產出 → 一筆發布紀錄」,再用測試信箱驗證寄送流程。正式課程使用前,仍需確認學生個資、授權與學校政策。

常見問題

問題可能原因解法
沒看到選單尚未重新整理 Sheet重新整理瀏覽器頁面
授權卡住第一次執行 Apps Script 需授權依畫面完成授權;若顯示未驗證,點選進階繼續
找不到週次尚未建立週報資料先執行建立本週課程週報
欄位數不符setValues 的資料列欄數與範圍不同檢查陣列每一列欄數是否一致
Email 格式被擋下學生名單 Email 不符合基本格式請學生重新登錄,或到 07_學生名單 修正 Email
沒有有效收件者尚未登錄學生,或訂閱欄位不是「是」先用學生首次登錄表單建立測試資料,再檢查訂閱狀態
Chat 發送失敗Webhook URL 未填或權限不符先將協作發布維持關閉,課後再測試

現場保底策略

3 小時研習的最低目標,是完成「一筆課程資料 → 三報產出 → 一筆發布紀錄」。若要測試 Email,先使用自己的測試信箱,不要在現場直接大量寄送正式學生名單。不要讓學員卡在進階功能;只要完成核心流程,課後就能再補 Docs、附件資料夾或正式通知。

現場狀況保底做法
GAS 授權失敗改用講師提供的示範 Sheet 操作
貼碼錯誤提供已完成版本,讓學員複製
來不及完成 Docs 自動產生至少完成三報文案產出
發布面板無法開啟直接從工作表複製三報內容
學生登錄表單無法開啟直接到 07_學生名單 手動輸入一筆測試學生資料
不適合現場寄送正式 Email只使用講師或自己的測試信箱驗證流程,正式寄送留到課後
進階學員太快完成嘗試修改三報模板、新增第四報,或測試學生名單摘要與 Email 寄送
現場帶課時,先保住核心工作流,再讓進度較快的學員嘗試客製模板、第四報或平台發布設定。

六、發布面板與前端體驗

讓學員從「操作工作表」進一步理解「工作表是資料庫,發布面板是使用介面」。

開啟「本週課程發布面板」後,側邊欄應呈現週次、日期區間、總狀態、下一步、Google Docs、附件資料夾與三報文案。新版 GAS 也會在中控台呈現學生名單摘要,協助確認是否已有可用的 Email 收件者。

發布面板的目的不是取代所有平台,也不是取代 Email 系統,而是讓教師知道同一份課程資料已經被轉譯到哪些地方,避免重複貼文、漏貼、誤寄或期末找不到資料。

區塊操作
學生學習報可貼到 LMS、Google Classroom、Moodle、LINE@ 或班級/課程群組,作為本週學習重點與下一步任務提醒。
週三報 Email可透過選單「寄送本週週三報給學生」寄給 07_學生名單 中已訂閱且 Email 有效的學生;系統會使用 BCC,避免學生彼此看見 Email。
課程紀錄報可留存在 Google Docs、Drive,必要時提供助教、系辦或課程團隊,作為教學歷程、協作事項與期末整理依據。
成果亮點報可貼到 FB 粉絲團、系所網站、Google 協作平台成果頁或招生素材,讓外部受眾理解課程成果與學生能力展現。
歷週索引建議彙整到 Google 協作平台,形成可回顧的課程網站。
發布紀錄記錄每一報是否已發布、發布到哪個平台、發布時間與負責人;Email 寄送後會寫入有效收件者人數。
學生名單摘要中控台顯示總登錄人數、訂閱週三報人數與有效 Email 人數,協助教師判斷是否可以進行寄送測試。
協作平台若有 Webhook,可發送到 Google Chat,提醒助教、共同授課教師或課程團隊處理後續事項。
快速連結開啟 Docs、附件資料夾、課程平台
發布面板仍是複製與追蹤中心;實際 Email 寄送由選單「寄送本週週三報給學生」完成,寄送前會確認收件者數量。研習現場建議只用測試信箱驗證流程,避免誤寄正式學生名單。

七、後續擴充方向

當天以 MVP 實作為主,擴充功能以展示與討論為主,不在 3 小時內硬做完。

Gemini 生成

條列轉公告、課程紀錄整理、成果敘事、摘要生成、語氣調整。提醒:AI 可協助改寫,但不應自動發布。

Google Chat 協作通知

適用共同授課、助教團隊、系辦行政協作、實習場域協作與專題課程多教師協作。

期末成果彙整

由歷週課程索引產生期末課程成果報告、教學實踐研究素材表、學生作品索引與課程反思草稿。

不同課程模板

一般講授課、實作課、實習課、專題課、服務學習、教學實踐研究課程都可有不同欄位。

從個人版到系所版

每位教師每週填課程週報
        ↓
系所自動彙整課程亮點
        ↓
產生系所成果週報
        ↓
提供招生、粉專、評鑑、成果報告使用

個人課程週報系所成果資料庫教學實踐素材招生與成果展示

八、研習教材包與成品要求

建議讓學員帶走可複製、可修改、可延伸的完整包。

建議準備四份教材

教材內容
主簡報課程資訊散落、一週三報概念、學理、系統總覽、操作示範、後續擴充
操作手冊安裝 GAS、初始化平台、填資料、產生三報、Docs、附件資料夾、發布面板、常見錯誤
範例資料表餐旅服務管理、幼兒活動設計、高齡服務方案設計
AI 修改指令包如何改欄位、改模板、新增第四報、串 Gemini、調整 Sidebar

3 小時結束時的學員成品

  1. 一份自己的 Google Sheet 課程週報平台
  2. 一筆課程週報資料
  3. 三種輸出文案:學生學習報、課程紀錄報、成果亮點報
  4. 一份 Google Docs 課程週報
  5. 一個課程照片/附件資料夾
  6. 一筆發布紀錄
  7. 一份歷週課程索引
  8. 一個後續擴充構想

講師時間控制

當天完成當天展示當天不要碰
GAS 貼上、初始化、建立週報、填資料、產三報、產 Docs、開 Sidebar、登錄測試學生、更新索引 Google Chat Webhook、Gemini API、正式學生 Email 寄送、期末成果自動彙整、多課程系所版 LINE API、Facebook Graph API、直接修改 LMS、未授權學生個資、成績資料、出缺席資料、大量正式寄信