一週三報|GAS 程式碼頁

高教課程版一週三報 GAS 程式碼

適用於大學課程、專題課、實作課與場域課程的一週三報工作流;已包含學生首次登錄、收件者名單與週三報 Email 寄送。

使用方式

研習現場建議:請優先使用本 HTML 頁的一鍵複製,不要直接在瀏覽器開啟 .gs 檔案複製,以避免瀏覽器或主機未正確宣告編碼時造成中文亂碼。
  1. 建立 Google Sheet。
  2. 開啟「擴充功能 → Apps Script」。
  3. 將下方 GAS 程式碼完整複製貼上。
  4. 儲存 Apps Script 專案。
  5. 回到試算表重新整理,依講義操作初始化流程。
  6. 若要測試 Email 功能,請先用自己的測試信箱完成學生首次登錄,不要在研習現場大量寄送正式學生名單。

取得 GAS 程式碼

本頁程式碼由專案內對應 .gs 原始檔產生,已透過 HTML 頁面宣告 UTF-8,適合研習現場直接複製。

適用版本:高教課程版 適用對象:大學授課教師、課程助教、教學行政支援者 程式來源:one_week_three_reports/一週三報_大學課程內容重點.gs 版本:依專案目前檔案內容 本次更新:學生首次登錄、07_學生名單、週三報 Email 寄送 頁面更新:2026-06-30
下載 .gs 原始檔
展開完整 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,請同步更新此 HTML 程式碼頁,避免研習現場使用到舊版本。