一週三報|GAS 程式碼頁

國中小班級版一週三報 GAS 程式碼

適用於班級週報、親師提醒、班級網站留存與成果故事整理。

使用方式

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

取得 GAS 程式碼

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

適用版本:國中小班級版 適用對象:國中小教師、導師、班級經營與親師溝通研習學員 程式來源:one_week_three_reports/一週三報.gs 版本:依專案目前檔案內容 頁面更新:2026-06-28
下載 .gs 原始檔
展開完整 GAS 程式碼
/**
 * 一週一報三端分流 GAS MVP
 *
 * 版本:MVP 0.4
 * 使用方式:
 * 1. 建立 Google Sheet
 * 2. 擴充功能 → Apps Script
 * 3. 貼上本檔
 * 4. 儲存並重新整理試算表
 * 5. 使用「一週一報三端分流」選單操作
 *
 * 核心功能:
 * - 初始化週報平台工作表
 * - 建立本週週報資料列
 * - 建立本週照片資料夾
 * - 產生三端文案:班網完整版、LINE 短提醒版、FB 故事版
 * - 產生 Google Docs 週報
 * - 提供 LINE / FB 文案複製側邊欄
 * - 產生班網發布素材與 HTML 貼上版
 * - 使用 Google Chat Webhook 發布週報摘要到協作平台
 * - 提供本週發布面板 Sidebar
 * - 標記發布狀態
 * - 產生精簡歷週索引
 *
 * 命名說明:
 * - onOpen 是 Google Apps Script 指定的簡單觸發器名稱。
 * - 其他自訂函式皆使用 OWTR_ 前綴,降低與其他 GAS 程式碼命名衝突的機率。
 */

const OWTR_CONFIG = {
  TIMEZONE: "Asia/Taipei",

  SHEETS: {
    DASHBOARD: "00_週報中控台",
    WEEKLY_DATA: "01_週報資料",
    COPY_OUTPUT: "02_三端文案",
    PUBLISH_LOG: "03_發布紀錄",
    SETTINGS: "04_設定",
    INDEX: "05_歷週索引",
    WEBSITE_MATERIALS: "06_班網發布素材",
  },

  DEFAULT_SETTINGS: [
    ["設定項目", "設定值", "說明"],
    ["班級名稱", "六年甲班", "顯示於週報標題"],
    ["導師名稱", "王老師", "顯示於週報署名"],
    ["學校名稱", "○○國小", "顯示於週報內容"],
    ["班級網站網址", "", "Google Sites 或班級網站網址"],
    ["FB粉專網址", "", "班級粉專網址,可空白"],
    ["Drive根資料夾ID", "", "可空白;空白時會自動建立一個根資料夾"],
    ["Google Chat Webhook URL", "", "協作平台 Google Chat 空間的 Webhook URL"],
    ["是否啟用協作平台發布", "否", "填「是」才允許一鍵發布到協作平台"],
    ["協作平台名稱", "Google Chat", "顯示於中控台與發布紀錄"],
    ["是否啟用AI", "否", "MVP 版暫不啟用 AI"],
  ],

  PLACEHOLDERS: {
    learningFocus: "國語:\n數學:\n自然/社會:\n其他:",
    activities: "本週班級活動或學校活動:",
    highlights: "學生亮點或值得鼓勵的表現:",
    reminders: "下週考試、繳交、活動、服裝或物品提醒:",
    parentTasks: "需要家長協助、簽名、填表或準備事項:",
  },

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

/**
 * 試算表開啟時建立選單
 */
function onOpen() {
  OWTR_onOpen();
}

function OWTR_onOpen() {
  SpreadsheetApp.getUi()
    .createMenu("一週一報三端分流")
    .addItem("1. 初始化平台", "OWTR_initPlatform")
    .addItem("重新整理中控台", "OWTR_refreshDashboardView")
    .addSeparator()
    .addItem("2. 建立本週週報範本", "OWTR_createCurrentWeekReport")
    .addItem("建立下一週週報範本", "OWTR_createNextWeekReport")
    .addItem("3. 建立本週照片資料夾", "OWTR_createPhotoFolderForActiveWeek")
    .addSeparator()
    .addItem("開啟本週發布面板", "OWTR_showWeeklyPublishPanel")
    .addSeparator()
    .addItem("4. 產生三端文案", "OWTR_generateThreeChannelCopies")
    .addItem("5. 產生 Google Docs 週報", "OWTR_generateGoogleDocForActiveWeek")
    .addItem("產生班網發布素材", "OWTR_generateWebsitePublishMaterials")
    .addItem("一鍵發布到協作平台", "OWTR_publishToCollaborationPlatform")
    .addSeparator()
    .addItem("複製 LINE 文案", "OWTR_showLineCopySidebar")
    .addItem("複製 FB 文案", "OWTR_showFbCopySidebar")
    .addSeparator()
    .addItem("6. 標記班網已發布", "OWTR_markWebsitePublished")
    .addItem("7. 標記 LINE 已發布", "OWTR_markLinePublished")
    .addItem("8. 標記 FB 已發布", "OWTR_markFbPublished")
    .addSeparator()
    .addItem("9. 更新歷週索引", "OWTR_updateWeeklyIndex")
    .addToUi();
}

/**
 * =========================
 * 初始化平台
 * =========================
 */

function OWTR_initPlatform() {
  OWTR_runAction_("初始化平台", function () {
    const ss = SpreadsheetApp.getActiveSpreadsheet();

    OWTR_setupDashboardSheet_(ss);
    OWTR_setupWeeklyDataSheet_(ss);
    OWTR_setupCopyOutputSheet_(ss);
    OWTR_setupPublishLogSheet_(ss);
    OWTR_setupSettingsSheet_(ss);
    OWTR_setupIndexSheet_(ss);
    OWTR_setupWebsiteMaterialsSheet_(ss);

    SpreadsheetApp.getUi().alert(
      "初始化完成。請先到「04_設定」確認班級資料,再執行「建立本週週報範本」。",
    );
  });
}

function OWTR_refreshDashboardView() {
  OWTR_runAction_("重新整理中控台", function () {
    const ss = SpreadsheetApp.getActiveSpreadsheet();
    const activeWeekId = OWTR_getActiveWeekId_() || OWTR_getLatestWeekId_();

    OWTR_ensureMvp03Structure_(ss);
    OWTR_setupDashboardSheet_(ss);
    OWTR_setupIndexSheet_(ss);

    if (activeWeekId) {
      OWTR_refreshDashboardForWeek_(activeWeekId);
      OWTR_updateWeeklyIndex_();
    }

    OWTR_alert_("中控台已重新整理,不會清空週報資料與三端文案。");
  });
}

function OWTR_setupDashboardSheet_(ss) {
  const sheet = OWTR_getOrCreateSheet_(ss, OWTR_CONFIG.SHEETS.DASHBOARD);
  sheet.clear();

  const values = [
    ["一週一報三端分流|班級週報中控台", "", "", "", "", ""],
    [
      "操作提示",
      "建立本週週報 → 填寫週報資料 → 產生三端文案 → 產生 Docs / 班網素材 → 手動發布三端或一鍵發布協作平台",
      "",
      "",
      "",
      "",
    ],
    ["", "", "", "", "", ""],
    ["目前週次ID", "", "", "總狀態", OWTR_CONFIG.STATUS.NOT_CREATED, ""],
    ["日期區間", "", "", "Google Docs", "尚未建立", ""],
    ["班級網站", "尚未設定", "", "照片資料夾", "尚未建立", ""],
    ["", "", "", "", "", ""],
    ["流程狀態", "", "", "", "", ""],
    ["草稿", "⚪ 尚未建立", "三端文案", "⚪ 尚未產生", "Google Docs", "⚪ 尚未產生"],
    ["三端發布", "⚪ 尚未完成", "協作平台", "⚪ 尚未發布", "", ""],
    ["", "", "", "", "", ""],
    ["發布狀態", "", "", "", "", ""],
    ["班級網站", "尚未發布", "", "", "", ""],
    ["LINE提醒", "尚未發布", "", "", "", ""],
    ["FB粉專", "尚未發布", "", "", "", ""],
    ["協作平台", "尚未發布", "", "", "", ""],
    ["", "", "", "", "", ""],
    ["本週流程步驟", "", "", "", "", ""],
    ["1", "建立週報範本", "⚪ 尚未完成", "下一步", "建立或確認本週週報資料列", ""],
    ["2", "填寫週報資料", "⚪ 尚未完成", "", "到 01_週報資料補齊內容", ""],
    ["3", "產生三端文案", "⚪ 尚未完成", "", "在發布面板或選單產生文案", ""],
    ["4", "建立 Docs / 照片資料夾", "⚪ 尚未完成", "", "在發布面板一鍵建立缺少項目", ""],
    ["5", "複製並發布三端", "⚪ 尚未完成", "", "從發布面板複製班網 / LINE / FB 文案", ""],
    ["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("#1f4e79");
  sheet.getRange("A2:F2").setBackground("#eaf3f8");
  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("A19:F24").setBackground("#f8fafc");
  sheet.getRange("E4").setFontWeight("bold");
  sheet.setColumnWidth(1, 150);
  sheet.setColumnWidth(2, 240);
  sheet.setColumnWidth(3, 140);
  sheet.setColumnWidth(4, 150);
  sheet.setColumnWidth(5, 240);
  sheet.setColumnWidth(6, 40);
  sheet.setFrozenRows(1);
}

function OWTR_setupWeeklyDataSheet_(ss) {
  const sheet = OWTR_getOrCreateSheet_(ss, OWTR_CONFIG.SHEETS.WEEKLY_DATA);
  sheet.clear();

  const headers = [
    "週次ID",
    "日期區間",
    "本週學習重點",
    "本週班級活動",
    "學生亮點",
    "下週重要提醒",
    "家長配合事項",
    "照片資料夾URL",
    "建立時間",
    "最後更新時間",
  ];

  sheet.getRange(1, 1, 1, headers.length).setValues([headers]);
  OWTR_styleHeader_(sheet, headers.length);
  sheet.setFrozenRows(1);
  sheet.setColumnWidths(1, headers.length, 180);
  sheet.setColumnWidth(3, 260);
  sheet.setColumnWidth(4, 260);
  sheet.setColumnWidth(5, 260);
  sheet.setColumnWidth(6, 260);
  sheet.setColumnWidth(7, 260);
}

function OWTR_setupCopyOutputSheet_(ss) {
  const sheet = OWTR_getOrCreateSheet_(ss, OWTR_CONFIG.SHEETS.COPY_OUTPUT);
  sheet.clear();

  const headers = [
    "週次ID",
    "班網完整版",
    "LINE短提醒版",
    "FB故事版",
    "產生時間",
    "是否確認",
  ];

  sheet.getRange(1, 1, 1, headers.length).setValues([headers]);
  OWTR_styleHeader_(sheet, headers.length);
  sheet.setFrozenRows(1);
  sheet.setColumnWidth(1, 150);
  sheet.setColumnWidth(2, 420);
  sheet.setColumnWidth(3, 360);
  sheet.setColumnWidth(4, 420);
}

function OWTR_setupPublishLogSheet_(ss) {
  const sheet = OWTR_getOrCreateSheet_(ss, OWTR_CONFIG.SHEETS.PUBLISH_LOG);
  sheet.clear();

  const headers = [
    "週次ID",
    "班級網站",
    "LINE",
    "FB",
    "協作平台",
    "Google Docs URL",
    "發布備註",
    "最後更新時間",
  ];

  sheet.getRange(1, 1, 1, headers.length).setValues([headers]);
  OWTR_styleHeader_(sheet, headers.length);
  sheet.setFrozenRows(1);
  sheet.setColumnWidths(1, headers.length, 180);
  sheet.setColumnWidth(6, 360);
}

function OWTR_setupSettingsSheet_(ss) {
  const sheet = OWTR_getOrCreateSheet_(ss, OWTR_CONFIG.SHEETS.SETTINGS);
  sheet.clear();

  sheet
    .getRange(1, 1, OWTR_CONFIG.DEFAULT_SETTINGS.length, 3)
    .setValues(OWTR_CONFIG.DEFAULT_SETTINGS);
  OWTR_styleHeader_(sheet, 3);
  sheet.setFrozenRows(1);
  sheet.setColumnWidth(1, 180);
  sheet.setColumnWidth(2, 360);
  sheet.setColumnWidth(3, 420);
}

function OWTR_setupIndexSheet_(ss) {
  const sheet = OWTR_getOrCreateSheet_(ss, OWTR_CONFIG.SHEETS.INDEX);
  sheet.clear();

  const headers = [
    "週次ID",
    "日期區間",
    "摘要",
    "Google Docs",
    "照片資料夾",
    "班級網站",
    "發布狀態",
    "最後更新時間",
  ];

  sheet.getRange(1, 1, 1, headers.length).setValues([headers]);
  OWTR_styleHeader_(sheet, headers.length);
  sheet.setFrozenRows(1);
  sheet.setColumnWidths(1, headers.length, 180);
  sheet.setColumnWidth(3, 420);
  sheet.setColumnWidth(4, 180);
  sheet.setColumnWidth(5, 180);
  sheet.setColumnWidth(6, 180);
  sheet.setColumnWidth(7, 240);
  sheet.setColumnWidth(8, 180);
}

function OWTR_setupWebsiteMaterialsSheet_(ss) {
  const sheet = OWTR_getOrCreateSheet_(ss, OWTR_CONFIG.SHEETS.WEBSITE_MATERIALS);
  sheet.clear();

  const headers = [
    "週次ID",
    "班網標題",
    "摘要",
    "純文字版",
    "HTML貼上版",
    "Google Docs連結",
    "照片資料夾連結",
    "產生時間",
  ];

  sheet.getRange(1, 1, 1, headers.length).setValues([headers]);
  OWTR_styleHeader_(sheet, headers.length);
  sheet.setFrozenRows(1);
  sheet.setColumnWidth(1, 150);
  sheet.setColumnWidth(2, 260);
  sheet.setColumnWidth(3, 360);
  sheet.setColumnWidth(4, 520);
  sheet.setColumnWidth(5, 520);
  sheet.setColumnWidth(6, 240);
  sheet.setColumnWidth(7, 240);
  sheet.setColumnWidth(8, 180);
}

function OWTR_ensureMvp03Structure_(ss) {
  OWTR_ensureSettingsRows_(ss);
  OWTR_ensurePublishLogColumns_(ss);
  OWTR_ensureWebsiteMaterialsSheet_(ss);
}

function OWTR_ensureSettingsRows_(ss) {
  const sheet = ss.getSheetByName(OWTR_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 = OWTR_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 OWTR_ensurePublishLogColumns_(ss) {
  const sheet = ss.getSheetByName(OWTR_CONFIG.SHEETS.PUBLISH_LOG);
  if (!sheet) return;

  const requiredHeaders = [
    "週次ID",
    "班級網站",
    "LINE",
    "FB",
    "協作平台",
    "Google Docs URL",
    "發布備註",
    "最後更新時間",
  ];
  const lastCol = Math.max(sheet.getLastColumn(), 1);
  const currentHeaders = sheet.getRange(1, 1, 1, lastCol).getValues()[0];

  if (currentHeaders.indexOf("協作平台") === -1) {
    sheet.insertColumnAfter(4);
  }

  sheet.getRange(1, 1, 1, requiredHeaders.length).setValues([requiredHeaders]);

  const lastRow = sheet.getLastRow();
  if (lastRow >= 2) {
    const collaborationRange = sheet.getRange(2, 5, lastRow - 1, 1);
    const values = collaborationRange.getValues().map(function (row) {
      return [row[0] || "尚未發布"];
    });
    collaborationRange.setValues(values);
  }

  OWTR_styleHeader_(sheet, requiredHeaders.length);
  sheet.setColumnWidths(1, requiredHeaders.length, 180);
  sheet.setColumnWidth(6, 360);
}

function OWTR_ensureWebsiteMaterialsSheet_(ss) {
  let sheet = ss.getSheetByName(OWTR_CONFIG.SHEETS.WEBSITE_MATERIALS);
  if (!sheet) {
    OWTR_setupWebsiteMaterialsSheet_(ss);
    return;
  }

  const headers = [
    "週次ID",
    "班網標題",
    "摘要",
    "純文字版",
    "HTML貼上版",
    "Google Docs連結",
    "照片資料夾連結",
    "產生時間",
  ];

  if (sheet.getLastRow() === 0 || sheet.getRange(1, 1).getValue() !== "週次ID") {
    sheet.getRange(1, 1, 1, headers.length).setValues([headers]);
  }

  OWTR_styleHeader_(sheet, headers.length);
  sheet.setFrozenRows(1);
}

/**
 * =========================
 * 建立週報
 * =========================
 */

function OWTR_createCurrentWeekReport() {
  OWTR_runAction_("建立本週週報範本", function () {
    const result = OWTR_createWeekReportByInfo_(OWTR_getCurrentWeekInfo_());
    if (!result) return;

    OWTR_alert_(
      result.existed
        ? "本週週報已存在:" + result.weekId
        : "已建立本週週報範本:" + result.weekId + "\n請到「01_週報資料」填寫內容。",
    );
  });
}

function OWTR_createNextWeekReport() {
  OWTR_runAction_("建立下一週週報範本", function () {
    const result = OWTR_createWeekReportByInfo_(OWTR_getNextWeekInfo_());
    if (!result) return;

    OWTR_alert_(
      result.existed
        ? "下一週週報已存在:" + result.weekId
        : "已建立下一週週報範本:" + result.weekId + "\n請到「01_週報資料」先填入可預備的提醒事項。",
    );
  });
}

function OWTR_createWeekReportByInfo_(weekInfo) {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const sheet = ss.getSheetByName(OWTR_CONFIG.SHEETS.WEEKLY_DATA);
  if (!sheet) {
    OWTR_alert_("請先執行「初始化平台」。");
    return null;
  }

  const weekId = weekInfo.weekId;
  const dateRange = weekInfo.dateRange;

  if (OWTR_findRowByWeekId_(sheet, weekId) > 0) {
    OWTR_refreshDashboardForWeek_(weekId);
    return { weekId: weekId, dateRange: dateRange, existed: true };
  }

  const now = new Date();
  const placeholders = OWTR_CONFIG.PLACEHOLDERS;

  sheet.appendRow([
    weekId,
    dateRange,
    placeholders.learningFocus,
    placeholders.activities,
    placeholders.highlights,
    placeholders.reminders,
    placeholders.parentTasks,
    "",
    now,
    now,
  ]);

  OWTR_ensurePublishLogRow_(weekId);
  OWTR_refreshDashboardForWeek_(weekId);
  OWTR_updateWeeklyIndex_();

  return { weekId: weekId, dateRange: dateRange, existed: false };
}

function OWTR_getCurrentWeekInfo_() {
  return OWTR_getWeekInfoForDate_(new Date());
}

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

function OWTR_getWeekInfoForDate_(today) {
  const timezone = OWTR_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 = OWTR_getWeekNumber_(today);

  const weekId = year + "-W" + String(weekNumber).padStart(2, "0");
  const dateRange =
    Utilities.formatDate(monday, timezone, "yyyy/MM/dd") +
    "–" +
    Utilities.formatDate(friday, timezone, "yyyy/MM/dd");

  return {
    weekId,
    dateRange,
  };
}

function OWTR_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 OWTR_createPhotoFolderForActiveWeek() {
  OWTR_runAction_("建立本週照片資料夾", function () {
    const activeWeekId = OWTR_getActiveWeekId_();
    if (!activeWeekId) {
      OWTR_alert_("目前沒有週次ID,請先建立本週週報範本。");
      return;
    }

    const beforeData = OWTR_getWeeklyDataByWeekId_(activeWeekId);
    const hadFolder = beforeData && beforeData.photoFolderUrl;
    const folderUrl = OWTR_createPhotoFolderForWeek_(activeWeekId);
    if (!folderUrl) return;

    OWTR_refreshDashboardForWeek_(activeWeekId);
    OWTR_updateWeeklyIndex_();

    OWTR_alert_((hadFolder ? "本週照片資料夾已存在:\n" : "已建立本週照片資料夾:\n") + folderUrl);
  });
}

function OWTR_createPhotoFolderForWeek_(weekId) {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const weeklySheet = ss.getSheetByName(OWTR_CONFIG.SHEETS.WEEKLY_DATA);

  if (!weeklySheet) {
    OWTR_alert_("請先執行「初始化平台」。");
    return "";
  }

  const row = OWTR_findRowByWeekId_(weeklySheet, weekId);
  if (row <= 0) {
    OWTR_alert_("找不到週報資料:" + weekId);
    return "";
  }

  const existingUrl = weeklySheet.getRange(row, 8).getValue();
  if (existingUrl) {
    return existingUrl;
  }

  const settings = OWTR_getSettings_();
  const className = settings["班級名稱"] || "班級";
  const rootFolder = OWTR_getOrCreateRootFolder_(settings);

  const folderName = weekId + "_" + className + "_週報照片";
  const folder = rootFolder.createFolder(folderName);
  const folderUrl = folder.getUrl();

  weeklySheet.getRange(row, 8).setValue(folderUrl);
  weeklySheet.getRange(row, 10).setValue(new Date());

  return folderUrl;
}

function OWTR_getOrCreateRootFolder_(settings) {
  const rootFolderId = settings["Drive根資料夾ID"];

  if (rootFolderId) {
    try {
      return DriveApp.getFolderById(rootFolderId);
    } catch (e) {
      OWTR_alert_(
        "「Drive根資料夾ID」無法讀取,系統將自動建立新的根資料夾。\n\n原始錯誤:" +
          e.message,
      );
    }
  }

  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const folderName = ss.getName() + "_一週一報資料夾";
  const folder = DriveApp.createFolder(folderName);

  const settingsSheet = ss.getSheetByName(OWTR_CONFIG.SHEETS.SETTINGS);
  const row = OWTR_findSettingsRow_("Drive根資料夾ID");
  if (settingsSheet && row > 0) {
    settingsSheet.getRange(row, 2).setValue(folder.getId());
  }

  return folder;
}

/**
 * =========================
 * 產生三端文案
 * =========================
 */

function OWTR_generateThreeChannelCopies() {
  OWTR_runAction_("產生三端文案", function () {
    const ss = SpreadsheetApp.getActiveSpreadsheet();
    const weeklySheet = ss.getSheetByName(OWTR_CONFIG.SHEETS.WEEKLY_DATA);
    const outputSheet = ss.getSheetByName(OWTR_CONFIG.SHEETS.COPY_OUTPUT);

    if (!weeklySheet || !outputSheet) {
      OWTR_alert_("請先執行「初始化平台」。");
      return;
    }

    const weekId = OWTR_getActiveWeekId_();
    if (!weekId) {
      OWTR_alert_("目前沒有週次ID,請先建立本週週報範本。");
      return;
    }

    const data = OWTR_getWeeklyDataByWeekId_(weekId);
    if (!data) {
      OWTR_alert_("找不到週報資料:" + weekId);
      return;
    }

    const settings = OWTR_getSettings_();
    const warnings = OWTR_getIncompleteFieldWarnings_(data);

    const websiteCopy = OWTR_buildWebsiteCopy_(data, settings);
    const lineCopy = OWTR_buildLineCopy_(data, settings);
    const fbCopy = OWTR_buildFbCopy_(data, settings);

    const existingRow = OWTR_findRowByWeekId_(outputSheet, weekId);
    const now = new Date();

    if (existingRow > 0) {
      outputSheet
        .getRange(existingRow, 2, 1, 5)
        .setValues([[websiteCopy, lineCopy, fbCopy, now, "否"]]);
    } else {
      outputSheet.appendRow([weekId, websiteCopy, lineCopy, fbCopy, now, "否"]);
    }

    OWTR_refreshDashboardForWeek_(weekId);
    OWTR_updateWeeklyIndex_();

    let message = "已產生三端文案。請到「02_三端文案」查看。";
    if (warnings.length > 0) {
      message +=
        "\n\n提醒:下列欄位仍是預設提示文字或尚未填寫,建議補齊後再正式發布:\n- " +
        warnings.join("\n- ");
    }
    OWTR_alert_(message);
  });
}

function OWTR_buildWebsiteCopy_(data, settings) {
  const className = settings["班級名稱"] || "班級";
  const teacherName = settings["導師名稱"] || "導師";
  const classWebsiteUrl = settings["班級網站網址"] || "";

  return [
    "【" + className + "|" + data.weekId + " 班級週報】",
    "",
    "日期區間:" + data.dateRange,
    "",
    "一、本週學習重點",
    OWTR_normalizeText_(data.learningFocus),
    "",
    "二、本週班級活動",
    OWTR_normalizeText_(data.activities),
    "",
    "三、學生亮點",
    OWTR_normalizeText_(data.highlights),
    "",
    "四、下週重要提醒",
    OWTR_normalizeText_(data.reminders),
    "",
    "五、家長配合事項",
    OWTR_normalizeText_(data.parentTasks),
    "",
    "六、照片與相關連結",
    data.photoFolderUrl ? "照片資料夾:" + data.photoFolderUrl : "本週尚未建立照片資料夾。",
    classWebsiteUrl ? "班級網站:" + classWebsiteUrl : "",
    "",
    teacherName + " 敬上",
  ]
    .filter(function (line, index, lines) {
      return line !== "" || lines[index - 1] !== "";
    })
    .join("\n")
    .trim();
}

function OWTR_buildLineCopy_(data, settings) {
  const className = settings["班級名稱"] || "班級";
  const websiteUrl = settings["班級網站網址"] || "";

  return [
    "【" + className + " 本週提醒|" + data.weekId + "】",
    "",
    "一、本週重點",
    OWTR_compactText_(data.learningFocus),
    "",
    "二、下週提醒",
    OWTR_compactText_(data.reminders),
    "",
    "三、家長配合",
    OWTR_compactText_(data.parentTasks),
    "",
    websiteUrl
      ? "完整週報請看班級網站:\n" + websiteUrl
      : "完整週報請見班級網站或老師後續公告。",
  ].join("\n");
}

function OWTR_buildFbCopy_(data, settings) {
  const className = settings["班級名稱"] || "班級";

  return [
    "【" + className + " 班級故事|" + data.weekId + "】",
    "",
    "這一週,孩子們在學習與班級活動中,持續累積屬於自己的成長。",
    "",
    "本週學習重點:",
    OWTR_compactText_(data.learningFocus),
    "",
    "班級活動與生活觀察:",
    OWTR_compactText_(data.activities),
    "",
    "特別想記錄的是:",
    OWTR_compactText_(data.highlights),
    "",
    "學習不只是完成課本進度,也是在一次次練習、合作與表達中,慢慢長出面對事情的能力。",
    "",
    data.photoFolderUrl ? "本週照片紀錄:\n" + data.photoFolderUrl : "",
  ]
    .join("\n")
    .trim();
}

function OWTR_getIncompleteFieldWarnings_(data) {
  const fields = [
    ["本週學習重點", "learningFocus", data.learningFocus],
    ["本週班級活動", "activities", data.activities],
    ["學生亮點", "highlights", data.highlights],
    ["下週重要提醒", "reminders", data.reminders],
    ["家長配合事項", "parentTasks", data.parentTasks],
  ];

  return fields
    .filter(function (field) {
      return OWTR_isPlaceholderOrBlank_(field[1], field[2]);
    })
    .map(function (field) {
      return field[0];
    });
}

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

/**
 * =========================
 * 產生 Google Docs 週報
 * =========================
 */

function OWTR_generateGoogleDocForActiveWeek() {
  OWTR_runAction_("產生 Google Docs 週報", function () {
    const weekId = OWTR_getActiveWeekId_();

    if (!weekId) {
      OWTR_alert_("目前沒有週次ID,請先建立本週週報範本。");
      return;
    }

    const docUrl = OWTR_createGoogleDocForWeek_(weekId);
    if (!docUrl) return;

    OWTR_refreshDashboardForWeek_(weekId);
    OWTR_updateWeeklyIndex_();

    OWTR_alert_("已產生 Google Docs 週報:\n" + docUrl);
  });
}

function OWTR_createGoogleDocForWeek_(weekId) {
  const existingDocUrl = OWTR_getPublishInfo_(weekId).docUrl;
  if (existingDocUrl) return existingDocUrl;

    const data = OWTR_getWeeklyDataByWeekId_(weekId);
    if (!data) {
      OWTR_alert_("找不到週報資料:" + weekId);
      return "";
    }

    const settings = OWTR_getSettings_();
    const className = settings["班級名稱"] || "班級";
    const title = className + "_" + weekId + "_班級週報";

    const doc = DocumentApp.create(title);
    const body = doc.getBody();
    body.clear();
    body.setMarginTop(54);
    body.setMarginBottom(54);
    body.setMarginLeft(54);
    body.setMarginRight(54);

    OWTR_buildFormalDocBody_(body, data, settings);
    OWTR_buildFormalDocFooter_(doc, settings);

    doc.saveAndClose();

    const docUrl = doc.getUrl();
    OWTR_updatePublishLogDocUrl_(weekId, docUrl);

    return docUrl;
}

function OWTR_buildFormalDocBody_(body, data, settings) {
  const className = settings["班級名稱"] || "班級";
  const teacherName = settings["導師名稱"] || "導師";
  const schoolName = settings["學校名稱"] || "";
  const classWebsiteUrl = settings["班級網站網址"] || "";

  body
    .appendParagraph(className + " 班級週報")
    .setHeading(DocumentApp.ParagraphHeading.TITLE)
    .setAlignment(DocumentApp.HorizontalAlignment.CENTER);

  const dateParagraph = body
    .appendParagraph(data.weekId + "|" + data.dateRange)
    .setAlignment(DocumentApp.HorizontalAlignment.CENTER);
  OWTR_setParagraphTextColor_(dateParagraph, "#666666");

  if (schoolName) {
    const schoolParagraph = body
      .appendParagraph(schoolName)
      .setAlignment(DocumentApp.HorizontalAlignment.CENTER);
    OWTR_setParagraphTextColor_(schoolParagraph, "#666666");
  }

  body.appendHorizontalRule();

  OWTR_appendDocSection_(body, "一、本週學習重點", data.learningFocus);
  OWTR_appendDocSection_(body, "二、本週班級活動", data.activities);
  OWTR_appendDocSection_(body, "三、學生亮點", data.highlights);
  OWTR_appendDocSection_(body, "四、下週重要提醒", data.reminders);
  OWTR_appendDocSection_(body, "五、家長配合事項", data.parentTasks);

  body
    .appendParagraph("六、照片與相關連結")
    .setHeading(DocumentApp.ParagraphHeading.HEADING2);
  if (data.photoFolderUrl) {
    const photoParagraph = body.appendParagraph("照片資料夾:");
    photoParagraph.appendText(data.photoFolderUrl).setLinkUrl(data.photoFolderUrl);
  } else {
    body.appendParagraph("本週尚未建立照片資料夾。");
  }

  if (classWebsiteUrl) {
    const siteParagraph = body.appendParagraph("班級網站:");
    siteParagraph.appendText(classWebsiteUrl).setLinkUrl(classWebsiteUrl);
  }

  body.appendParagraph("");
  body.appendHorizontalRule();
  body
    .appendParagraph(teacherName + " 敬上")
    .setAlignment(DocumentApp.HorizontalAlignment.RIGHT);
  const generatedDateParagraph = body
    .appendParagraph(
      Utilities.formatDate(new Date(), OWTR_CONFIG.TIMEZONE, "yyyy/MM/dd"),
    )
    .setAlignment(DocumentApp.HorizontalAlignment.RIGHT);
  OWTR_setParagraphTextColor_(generatedDateParagraph, "#666666");
}

function OWTR_appendDocSection_(body, heading, text) {
  body.appendParagraph(heading).setHeading(DocumentApp.ParagraphHeading.HEADING2);

  const lines = OWTR_normalizeText_(text)
    .split("\n")
    .map(function (line) {
      return line.trim();
    })
    .filter(function (line) {
      return line !== "";
    });

  lines.forEach(function (line) {
    body.appendParagraph(line).setLineSpacing(1.15);
  });
}

function OWTR_buildFormalDocFooter_(doc, settings) {
  const footer = doc.addFooter();
  const className = settings["班級名稱"] || "班級";
  const teacherName = settings["導師名稱"] || "導師";
  const footerParagraph = footer
    .appendParagraph(className + "|" + teacherName)
    .setAlignment(DocumentApp.HorizontalAlignment.CENTER);
  OWTR_setParagraphTextColor_(footerParagraph, "#888888");
}

/**
 * =========================
 * 班網發布素材
 * =========================
 */

function OWTR_generateWebsitePublishMaterials() {
  OWTR_runAction_("產生班網發布素材", function () {
    const weekId = OWTR_getActiveWeekId_();
    if (!weekId) {
      OWTR_alert_("目前沒有週次ID,請先建立本週週報範本。");
      return;
    }

    const result = OWTR_generateWebsitePublishMaterialsForWeek_(weekId);
    if (!result) return;

    OWTR_alert_("已產生班網發布素材,請到「06_班網發布素材」查看。");
  });
}

function OWTR_generateWebsitePublishMaterialsForWeek_(weekId) {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  OWTR_ensureWebsiteMaterialsSheet_(ss);

  const data = OWTR_getWeeklyDataByWeekId_(weekId);
  if (!data) {
    OWTR_alert_("找不到週報資料:" + weekId);
    return null;
  }

  const settings = OWTR_getSettings_();
  const publishInfo = OWTR_getPublishInfo_(weekId);
  const websiteCopy = OWTR_getWebsiteCopyForWeek_(weekId) || OWTR_buildWebsiteCopy_(data, settings);
  const title = OWTR_buildWebsiteMaterialTitle_(data, settings);
  const summary = OWTR_makeWeeklySummary_(data);
  const html = OWTR_buildWebsiteHtml_(title, summary, websiteCopy, publishInfo.docUrl, data.photoFolderUrl);
  const sheet = ss.getSheetByName(OWTR_CONFIG.SHEETS.WEBSITE_MATERIALS);
  const existingRow = OWTR_findRowByWeekId_(sheet, weekId);
  const rowValues = [
    weekId,
    title,
    summary,
    websiteCopy,
    html,
    publishInfo.docUrl || "",
    data.photoFolderUrl || "",
    new Date(),
  ];

  let row = existingRow;
  if (existingRow > 0) {
    sheet.getRange(existingRow, 1, 1, rowValues.length).setValues([rowValues]);
  } else {
    sheet.appendRow(rowValues);
    row = sheet.getLastRow();
  }

  OWTR_setLinkCell_(sheet, row, 6, "開啟 Docs", publishInfo.docUrl, "尚未建立");
  OWTR_setLinkCell_(sheet, row, 7, "開啟照片資料夾", data.photoFolderUrl, "尚未建立");

  return {
    title: title,
    summary: summary,
    plainText: websiteCopy,
    html: html,
    docUrl: publishInfo.docUrl || "",
    photoFolderUrl: data.photoFolderUrl || "",
  };
}

function OWTR_getWebsiteCopyForWeek_(weekId) {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const outputSheet = ss.getSheetByName(OWTR_CONFIG.SHEETS.COPY_OUTPUT);
  if (!outputSheet) return "";

  const row = OWTR_findRowByWeekId_(outputSheet, weekId);
  if (row <= 0) return "";

  return outputSheet.getRange(row, 2).getValue();
}

function OWTR_buildWebsiteMaterialTitle_(data, settings) {
  const className = settings["班級名稱"] || "班級";
  return className + "|" + data.weekId + " 班級週報";
}

function OWTR_buildWebsiteHtml_(title, summary, plainText, docUrl, photoFolderUrl) {
  const paragraphs = OWTR_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="' + OWTR_escapeHtml_(docUrl) + '">Google Docs 週報</a></li>');
  }
  if (photoFolderUrl) {
    links.push('<li><a href="' + OWTR_escapeHtml_(photoFolderUrl) + '">本週照片資料夾</a></li>');
  }

  return [
    '<article class="weekly-report">',
    "  <h1>" + OWTR_escapeHtml_(title) + "</h1>",
    '  <p class="weekly-report-summary">' + OWTR_escapeHtml_(summary) + "</p>",
    '  <div class="weekly-report-body">',
    paragraphs,
    "  </div>",
    links.length > 0 ? '  <ul class="weekly-report-links">' + links.join("") + "</ul>" : "",
    "</article>",
  ]
    .filter(function (line) {
      return line !== "";
    })
    .join("\n");
}

/**
 * =========================
 * 協作平台發布
 * =========================
 */

function OWTR_publishToCollaborationPlatform() {
  OWTR_runAction_("一鍵發布到協作平台", function () {
    const weekId = OWTR_getActiveWeekId_();
    if (!weekId) {
      OWTR_alert_("目前沒有週次ID,請先建立本週週報範本。");
      return;
    }

    const settings = OWTR_getSettings_();
    const isEnabled = String(settings["是否啟用協作平台發布"] || "").trim() === "是";
    const webhookUrl = String(settings["Google Chat Webhook URL"] || "").trim();
    const platformName = String(settings["協作平台名稱"] || "Google Chat").trim();

    if (!isEnabled) {
      OWTR_alert_("尚未啟用協作平台發布。請到「04_設定」將「是否啟用協作平台發布」填為「是」。");
      return;
    }

    if (!webhookUrl) {
      OWTR_alert_("尚未設定 Google Chat Webhook URL。請先到「04_設定」填入 Webhook URL。");
      return;
    }

    if (!OWTR_hasThreeChannelCopies_(weekId)) {
      OWTR_alert_("三端文案尚未產生。請先執行「產生三端文案」,確認內容後再一鍵發布到協作平台。");
      return;
    }

    let data = OWTR_getWeeklyDataByWeekId_(weekId);
    if (!data) {
      OWTR_alert_("找不到週報資料:" + weekId);
      return;
    }

    let publishInfo = OWTR_getPublishInfo_(weekId);
    let docUrl = publishInfo.docUrl;
    if (!docUrl) {
      const shouldCreateDoc = OWTR_confirm_(
        "Google Docs 尚未建立",
        "是否立即建立 Google Docs 週報後再發布到協作平台?",
      );
      if (!shouldCreateDoc) return;
      docUrl = OWTR_createGoogleDocForWeek_(weekId);
      if (!docUrl) return;
    }

    let photoFolderUrl = data.photoFolderUrl;
    if (!photoFolderUrl) {
      const shouldCreateFolder = OWTR_confirm_(
        "照片資料夾尚未建立",
        "是否立即建立本週照片資料夾後再發布到協作平台?",
      );
      if (!shouldCreateFolder) return;
      photoFolderUrl = OWTR_createPhotoFolderForWeek_(weekId);
      if (!photoFolderUrl) return;
      data = OWTR_getWeeklyDataByWeekId_(weekId);
    }

    const materials = OWTR_generateWebsitePublishMaterialsForWeek_(weekId);
    if (!materials) return;

    publishInfo = OWTR_getPublishInfo_(weekId);
    const message = OWTR_buildGoogleChatMessage_(
      weekId,
      data,
      settings,
      publishInfo,
      docUrl,
      photoFolderUrl,
    );

    OWTR_sendGoogleChatWebhook_(webhookUrl, message);
    OWTR_updatePublishLogCollaborationStatus_(weekId, platformName);
    OWTR_refreshDashboardForWeek_(weekId);
    OWTR_updateWeeklyIndex_();

    OWTR_alert_("已發布週報摘要到「" + platformName + "」,並更新發布紀錄。");
  });
}

function OWTR_buildGoogleChatMessage_(weekId, data, settings, publishInfo, docUrl, photoFolderUrl) {
  const className = settings["班級名稱"] || "班級";
  const platformName = settings["協作平台名稱"] || "Google Chat";
  const lines = [
    "【" + className + " 週報協作通知|" + weekId + "】",
    "日期區間:" + data.dateRange,
    "",
    "摘要:",
    OWTR_makeWeeklySummary_(data),
    "",
    "Google Docs:",
    docUrl || "尚未建立",
    "",
    "照片資料夾:",
    photoFolderUrl || "尚未建立",
    "",
    "三端發布狀態:",
    "班網:" + (publishInfo.websiteStatus || "尚未發布"),
    "LINE:" + (publishInfo.lineStatus || "尚未發布"),
    "FB:" + (publishInfo.fbStatus || "尚未發布"),
    "",
    "協作平台:" + platformName,
  ];

  return lines.join("\n");
}

function OWTR_sendGoogleChatWebhook_(webhookUrl, message) {
  const response = UrlFetchApp.fetch(webhookUrl, {
    method: "post",
    contentType: "application/json; charset=UTF-8",
    payload: JSON.stringify({ text: message }),
    muteHttpExceptions: true,
  });
  const responseCode = response.getResponseCode();

  if (responseCode < 200 || responseCode >= 300) {
    throw new Error(
      "Google Chat Webhook 發送失敗,HTTP " +
        responseCode +
        ":" +
        response.getContentText(),
    );
  }
}

function OWTR_updatePublishLogCollaborationStatus_(weekId, platformName) {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const sheet = ss.getSheetByName(OWTR_CONFIG.SHEETS.PUBLISH_LOG);
  if (!sheet) {
    OWTR_alert_("找不到「03_發布紀錄」,請先執行「初始化平台」。");
    return;
  }

  OWTR_ensurePublishLogRow_(weekId);

  const row = OWTR_findRowByWeekId_(sheet, weekId);
  const nowText = Utilities.formatDate(
    new Date(),
    OWTR_CONFIG.TIMEZONE,
    "yyyy/MM/dd HH:mm",
  );
  sheet.getRange(row, 5).setValue("已發布到 " + platformName + " " + nowText);
  sheet.getRange(row, 8).setValue(new Date());
}

/**
 * =========================
 * 本週發布面板
 * =========================
 */

function OWTR_showWeeklyPublishPanel() {
  OWTR_runAction_("開啟本週發布面板", function () {
    const html = HtmlService.createHtmlOutput(OWTR_buildWeeklyPublishPanelHtml_())
      .setTitle("本週發布面板");
    SpreadsheetApp.getUi().showSidebar(html);
  });
}

function OWTR_getWeeklyPublishPanelData() {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  OWTR_ensureMvp03Structure_(ss);

  const weekId = OWTR_getActiveWeekId_() || OWTR_getLatestWeekId_();
  const settings = OWTR_getSettings_();
  const emptyData = {
    hasWeek: false,
    weekId: "",
    dateRange: "",
    overallStatus: OWTR_CONFIG.STATUS.NOT_CREATED,
    nextAction: "請先建立本週週報範本。",
    classWebsiteUrl: settings["班級網站網址"] || "",
    platformName: settings["協作平台名稱"] || "Google Chat",
    copiesReady: false,
    docReady: false,
    photoReady: false,
    materialsReady: false,
    links: {
      docUrl: "",
      photoFolderUrl: "",
      classWebsiteUrl: settings["班級網站網址"] || "",
    },
    publishInfo: OWTR_getPublishInfo_(""),
    copies: {
      websitePlain: "",
      websiteHtml: "",
      line: "",
      fb: "",
      collaborationPreview: "",
    },
  };

  if (!weekId) return emptyData;

  const weeklyData = OWTR_getWeeklyDataByWeekId_(weekId);
  if (!weeklyData) return emptyData;

  const publishInfo = OWTR_getPublishInfo_(weekId);
  const copies = OWTR_getThreeChannelCopiesForWeek_(weekId);
  const copiesReady = Boolean(copies.website || copies.line || copies.fb);
  const websiteSummary = OWTR_makeWeeklySummary_(weeklyData);
  const websiteTitle = OWTR_buildWebsiteMaterialTitle_(weeklyData, settings);
  const websitePlain =
    copies.website || OWTR_buildWebsiteCopy_(weeklyData, settings);
  const websiteHtml = OWTR_buildWebsiteHtml_(
    websiteTitle,
    websiteSummary,
    websitePlain,
    publishInfo.docUrl,
    weeklyData.photoFolderUrl,
  );
  const collaborationPreview = OWTR_buildGoogleChatMessage_(
    weekId,
    weeklyData,
    settings,
    publishInfo,
    publishInfo.docUrl,
    weeklyData.photoFolderUrl,
  );
  const overallStatus = OWTR_getOverallStatus_(publishInfo, copiesReady);

  return {
    hasWeek: true,
    weekId: weekId,
    dateRange: weeklyData.dateRange,
    overallStatus: overallStatus,
    nextAction: OWTR_getNextActionText_(
      weeklyData,
      publishInfo,
      copiesReady,
    ),
    classWebsiteUrl: settings["班級網站網址"] || "",
    platformName: settings["協作平台名稱"] || "Google Chat",
    copiesReady: copiesReady,
    docReady: Boolean(publishInfo.docUrl),
    photoReady: Boolean(weeklyData.photoFolderUrl),
    materialsReady: copiesReady,
    links: {
      docUrl: publishInfo.docUrl || "",
      photoFolderUrl: weeklyData.photoFolderUrl || "",
      classWebsiteUrl: settings["班級網站網址"] || "",
    },
    publishInfo: publishInfo,
    copies: {
      websitePlain: websitePlain,
      websiteHtml: websiteHtml,
      line: copies.line || "",
      fb: copies.fb || "",
      collaborationPreview: collaborationPreview,
    },
  };
}

function OWTR_getThreeChannelCopiesForWeek_(weekId) {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const outputSheet = ss.getSheetByName(OWTR_CONFIG.SHEETS.COPY_OUTPUT);
  if (!outputSheet) {
    return { website: "", line: "", fb: "" };
  }

  const row = OWTR_findRowByWeekId_(outputSheet, weekId);
  if (row <= 0) {
    return { website: "", line: "", fb: "" };
  }

  const values = outputSheet.getRange(row, 1, 1, 6).getValues()[0];
  return {
    website: values[1] || "",
    line: values[2] || "",
    fb: values[3] || "",
  };
}

function OWTR_getNextActionText_(weeklyData, publishInfo, copiesReady) {
  if (!weeklyData) return "請先建立本週週報範本。";
  if (OWTR_getIncompleteFieldWarnings_(weeklyData).length > 0) {
    return "請先補齊 01_週報資料中仍是提示文字的欄位。";
  }
  if (!copiesReady) return "請產生三端文案。";
  if (!publishInfo.docUrl) return "請建立 Google Docs 週報。";
  if (!weeklyData.photoFolderUrl) return "請建立本週照片資料夾。";
  if (!OWTR_isPublished_(publishInfo.websiteStatus)) {
    return "請複製班網素材,貼到班級網站後標記班網已發布。";
  }
  if (!OWTR_isPublished_(publishInfo.lineStatus)) {
    return "請複製 LINE 文案並標記 LINE 已發布。";
  }
  if (!OWTR_isPublished_(publishInfo.fbStatus)) {
    return "請複製 FB 文案並標記 FB 已發布。";
  }
  if (!OWTR_isPublished_(publishInfo.collaborationStatus)) {
    return "可發布到協作平台,讓共備或行政協作空間同步掌握狀態。";
  }
  return "本週發布流程已完成。";
}

function OWTR_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;}",
    ".topbar{display:flex;align-items:flex-start;justify-content:space-between;gap:10px;margin-bottom:12px;}",
    "h1{font-size:18px;line-height:1.25;margin:0;color:#102a43;}",
    ".muted{color:#667085;font-size:12px;line-height:1.45;}",
    ".status{display:inline-flex;align-items:center;justify-content:center;border:1px solid #d6dee8;border-radius:8px;background:#fff;padding:6px 8px;font-weight:700;color:#25364b;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;display:flex;justify-content:space-between;gap:8px;align-items:center;}",
    ".grid{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin:10px 0;}",
    ".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;}",
    ".field div,.field a{font-size:12px;line-height:1.35;word-break:break-word;}",
    "a{color:#1155cc;text-decoration:underline;}",
    "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;}",
    "textarea.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;line-height:1.2;padding:9px 10px;cursor:pointer;}",
    "button.primary{background:#1f4e79;border-color:#1f4e79;color:#fff;}",
    "button.success{background:#087443;border-color:#087443;color:#fff;}",
    "button.warn{background:#9a5b00;border-color:#9a5b00;color:#fff;}",
    "button:disabled{opacity:.55;cursor:wait;}",
    ".pill{font-size:11px;border-radius:999px;padding:4px 7px;background:#eef2f7;color:#344054;white-space:nowrap;}",
    ".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="topbar"><div><h1>本週發布面板</h1><div class="muted" id="weekMeta">讀取中...</div></div><div class="status" id="overallStatus">...</div></div>',
    '<div id="root"></div>',
    '<div id="toast"></div>',
    "</div>",
    "<script>",
    "var state=null;",
    "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\" rel=\"noopener\">'+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 setBusy(on){Array.prototype.forEach.call(document.querySelectorAll('button'),function(btn){btn.disabled=on;});}",
    "function copyFrom(id,label){var el=document.getElementById(id);if(!el){return;} el.focus();el.select();var value=el.value;if(navigator.clipboard){navigator.clipboard.writeText(value).then(function(){toast(label+'已複製。');}).catch(function(){document.execCommand('copy');toast(label+'已選取,請使用快捷鍵複製。');});}else{document.execCommand('copy');toast(label+'已選取,請使用快捷鍵複製。');}}",
    "function run(fn){setBusy(true);var runner=google.script.run.withSuccessHandler(function(){setBusy(false);load();}).withFailureHandler(function(err){setBusy(false);toast(err&&err.message?err.message:String(err));});if(fn==='OWTR_createCurrentWeekReport'){runner.OWTR_createCurrentWeekReport();}else if(fn==='OWTR_createNextWeekReport'){runner.OWTR_createNextWeekReport();}else if(fn==='OWTR_generateThreeChannelCopies'){runner.OWTR_generateThreeChannelCopies();}else if(fn==='OWTR_generateGoogleDocForActiveWeek'){runner.OWTR_generateGoogleDocForActiveWeek();}else if(fn==='OWTR_createPhotoFolderForActiveWeek'){runner.OWTR_createPhotoFolderForActiveWeek();}else if(fn==='OWTR_markWebsitePublished'){runner.OWTR_markWebsitePublished();}else if(fn==='OWTR_markLinePublished'){runner.OWTR_markLinePublished();}else if(fn==='OWTR_markFbPublished'){runner.OWTR_markFbPublished();}else if(fn==='OWTR_publishToCollaborationPlatform'){runner.OWTR_publishToCollaborationPlatform();}else{setBusy(false);toast('未知操作:'+fn);}}",
    "function load(){google.script.run.withSuccessHandler(function(data){state=data;render(data);}).withFailureHandler(function(err){toast(err&&err.message?err.message:String(err));}).OWTR_getWeeklyPublishPanelData();}",
    "function render(data){document.getElementById('weekMeta').textContent=data.hasWeek?(data.weekId+'|'+data.dateRange):'尚未建立週報';document.getElementById('overallStatus').textContent=data.overallStatus||'尚未建立';var html='';if(!data.hasWeek){html+='<div class=\"notice\">尚未建立本週週報範本。請先建立本週範本,或建立下一週範本。</div><div class=\"actions\"><button class=\"primary\" onclick=\"run(\\'OWTR_createCurrentWeekReport\\')\">建立本週週報範本</button><button onclick=\"run(\\'OWTR_createNextWeekReport\\')\">建立下一週週報範本</button></div>';document.getElementById('root').innerHTML=html;return;}html+='<div class=\"notice\"><strong>下一步:</strong>'+esc(data.nextAction)+'</div>';html+='<div class=\"grid\"><div class=\"field\"><label>Google Docs</label><div>'+link('開啟 Docs',data.links.docUrl)+'</div></div><div class=\"field\"><label>照片資料夾</label><div>'+link('開啟照片資料夾',data.links.photoFolderUrl)+'</div></div><div class=\"field\"><label>班級網站</label><div>'+link('開啟班級網站',data.links.classWebsiteUrl)+'</div></div><div class=\"field\"><label>文案狀態</label><div>'+(data.copiesReady?pill(true,'已產生三端文案'):pill(false,'尚未產生文案'))+'</div></div></div>';if(!data.copiesReady){html+='<div class=\"notice\">尚未產生三端文案。請先產生文案,再進行複製與發布。</div><div class=\"actions\"><button class=\"primary\" onclick=\"run(\\'OWTR_generateThreeChannelCopies\\')\">產生三端文案</button></div>';}if(!data.docReady||!data.photoReady){html+='<div class=\"panel\"><h2>發布前準備 <span class=\"pill missing\">可補齊</span></h2><div class=\"actions two\">';if(!data.docReady){html+='<button onclick=\"run(\\'OWTR_generateGoogleDocForActiveWeek\\')\">建立 Google Docs</button>';}if(!data.photoReady){html+='<button onclick=\"run(\\'OWTR_createPhotoFolderForActiveWeek\\')\">建立照片資料夾</button>';}html+='</div></div>';}html+=sectionWebsite(data);html+=sectionLine(data);html+=sectionFb(data);html+=sectionCollab(data);document.getElementById('root').innerHTML=html;}",
    "function sectionWebsite(data){return '<div class=\"panel\"><h2>班網版 '+pill(data.copiesReady,'純文字 / HTML')+'</h2><label class=\"muted\">純文字版</label><textarea id=\"websitePlain\">'+esc(data.copies.websitePlain)+'</textarea><label class=\"muted\">HTML 貼上版</label><textarea class=\"html\" id=\"websiteHtml\">'+esc(data.copies.websiteHtml)+'</textarea><div class=\"actions two\"><button onclick=\"copyFrom(\\'websitePlain\\',\\'班網純文字\\')\">複製純文字</button><button onclick=\"copyFrom(\\'websiteHtml\\',\\'班網HTML\\')\">複製HTML</button><button class=\"success\" onclick=\"run(\\'OWTR_markWebsitePublished\\')\">標記班網已發布</button></div><div class=\"muted\">目前狀態:'+esc(data.publishInfo.websiteStatus)+'</div></div>';}",
    "function sectionLine(data){return '<div class=\"panel\"><h2>LINE版 '+pill(Boolean(data.copies.line),'複製發布')+'</h2><textarea id=\"lineCopy\">'+esc(data.copies.line)+'</textarea><div class=\"actions two\"><button onclick=\"copyFrom(\\'lineCopy\\',\\'LINE文案\\')\">複製LINE文案</button><button class=\"success\" onclick=\"run(\\'OWTR_markLinePublished\\')\">標記LINE已發布</button></div><div class=\"muted\">目前狀態:'+esc(data.publishInfo.lineStatus)+'</div></div>';}",
    "function sectionFb(data){return '<div class=\"panel\"><h2>FB版 '+pill(Boolean(data.copies.fb),'複製發布')+'</h2><textarea id=\"fbCopy\">'+esc(data.copies.fb)+'</textarea><div class=\"actions two\"><button onclick=\"copyFrom(\\'fbCopy\\',\\'FB文案\\')\">複製FB文案</button><button class=\"success\" onclick=\"run(\\'OWTR_markFbPublished\\')\">標記FB已發布</button></div><div class=\"muted\">目前狀態:'+esc(data.publishInfo.fbStatus)+'</div></div>';}",
    "function sectionCollab(data){return '<div class=\"panel\"><h2>協作平台 '+pill(Boolean(data.publishInfo.collaborationStatus&&data.publishInfo.collaborationStatus.indexOf('已發布')===0),esc(data.platformName))+'</h2><textarea id=\"collabPreview\">'+esc(data.copies.collaborationPreview)+'</textarea><div class=\"actions\"><button class=\"primary\" onclick=\"run(\\'OWTR_publishToCollaborationPlatform\\')\">發布到協作平台</button></div><div class=\"muted\">目前狀態:'+esc(data.publishInfo.collaborationStatus)+'</div></div>';}",
    "load();",
    "</script>",
    "</body>",
    "</html>",
  ].join("");
}

/**
 * =========================
 * 複製文案側邊欄
 * =========================
 */

function OWTR_showLineCopySidebar() {
  OWTR_runAction_("開啟 LINE 文案複製側邊欄", function () {
    OWTR_showCopySidebar_("LINE");
  });
}

function OWTR_showFbCopySidebar() {
  OWTR_runAction_("開啟 FB 文案複製側邊欄", function () {
    OWTR_showCopySidebar_("FB");
  });
}

function OWTR_showCopySidebar_(channelName) {
  const weekId = OWTR_getActiveWeekId_();
  if (!weekId) {
    OWTR_alert_("目前沒有週次ID,請先建立本週週報範本。");
    return;
  }

  const copy = OWTR_getCopyForSidebar_(weekId, channelName);
  if (!copy) {
    OWTR_alert_("尚未產生三端文案,請先執行「產生三端文案」。");
    return;
  }

  const title = channelName + " 文案";
  const html = HtmlService.createHtmlOutput(
    OWTR_buildCopySidebarHtml_(title, weekId, copy),
  ).setTitle("複製 " + title);

  SpreadsheetApp.getUi().showSidebar(html);
}

function OWTR_getCopyForSidebar_(weekId, channelName) {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const outputSheet = ss.getSheetByName(OWTR_CONFIG.SHEETS.COPY_OUTPUT);
  if (!outputSheet) return "";

  const row = OWTR_findRowByWeekId_(outputSheet, weekId);
  if (row <= 0) return "";

  if (channelName === "LINE") return outputSheet.getRange(row, 3).getValue();
  if (channelName === "FB") return outputSheet.getRange(row, 4).getValue();
  return "";
}

function OWTR_buildCopySidebarHtml_(title, weekId, copy) {
  const escapedTitle = OWTR_escapeHtml_(title);
  const escapedWeekId = OWTR_escapeHtml_(weekId);
  const escapedCopy = OWTR_escapeHtml_(copy);

  return [
    "<!doctype html>",
    "<html>",
    "<head>",
    '<base target="_top">',
    "<style>",
    "body{font-family:Arial,'Noto Sans TC',sans-serif;margin:0;padding:16px;background:#f8fafc;color:#1f2937;}",
    "h1{font-size:18px;margin:0 0 4px;}",
    ".meta{font-size:12px;color:#6b7280;margin-bottom:12px;}",
    "textarea{box-sizing:border-box;width:100%;height:360px;border:1px solid #cbd5e1;border-radius:8px;padding:12px;font-size:14px;line-height:1.5;resize:vertical;background:#fff;color:#111827;}",
    "button{width:100%;margin-top:12px;border:0;border-radius:8px;background:#1f4e79;color:#fff;font-size:15px;font-weight:700;padding:11px 12px;cursor:pointer;}",
    "button:active{transform:translateY(1px);}",
    ".hint{font-size:12px;color:#6b7280;margin-top:10px;line-height:1.5;}",
    "#status{font-size:13px;margin-top:10px;color:#047857;min-height:18px;}",
    "</style>",
    "</head>",
    "<body>",
    "<h1>" + escapedTitle + "</h1>",
    '<div class="meta">' + escapedWeekId + "</div>",
    '<textarea id="copyText">' + escapedCopy + "</textarea>",
    '<button onclick="copyText()">複製文案</button>',
    '<div id="status"></div>',
    '<div class="hint">若瀏覽器阻擋自動複製,請點進文字框後使用 Ctrl+C 或 Command+C。</div>',
    "<script>",
    "function copyText(){",
    "var textarea=document.getElementById('copyText');",
    "textarea.focus();",
    "textarea.select();",
    "var status=document.getElementById('status');",
    "if(navigator.clipboard&&window.isSecureContext){",
    "navigator.clipboard.writeText(textarea.value).then(function(){status.textContent='已複製到剪貼簿。';}).catch(function(){document.execCommand('copy');status.textContent='已選取文案,請使用快捷鍵複製。';});",
    "}else{",
    "document.execCommand('copy');status.textContent='已選取文案,請使用快捷鍵複製。';",
    "}",
    "}",
    "</script>",
    "</body>",
    "</html>",
  ].join("");
}

/**
 * =========================
 * 發布狀態
 * =========================
 */

function OWTR_markWebsitePublished() {
  OWTR_runAction_("標記班網已發布", function () {
    OWTR_markPublished_("班級網站");
  });
}

function OWTR_markLinePublished() {
  OWTR_runAction_("標記 LINE 已發布", function () {
    OWTR_markPublished_("LINE");
  });
}

function OWTR_markFbPublished() {
  OWTR_runAction_("標記 FB 已發布", function () {
    OWTR_markPublished_("FB");
  });
}

function OWTR_markPublished_(channelName) {
  const weekId = OWTR_getActiveWeekId_();

  if (!weekId) {
    OWTR_alert_("目前沒有週次ID,請先建立本週週報範本。");
    return;
  }

  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const sheet = ss.getSheetByName(OWTR_CONFIG.SHEETS.PUBLISH_LOG);
  if (!sheet) {
    OWTR_alert_("找不到「03_發布紀錄」,請先執行「初始化平台」。");
    return;
  }

  OWTR_ensurePublishLogRow_(weekId);

  const row = OWTR_findRowByWeekId_(sheet, weekId);
  const nowText = Utilities.formatDate(
    new Date(),
    OWTR_CONFIG.TIMEZONE,
    "yyyy/MM/dd HH:mm",
  );

  let col = 0;
  if (channelName === "班級網站") col = 2;
  if (channelName === "LINE") col = 3;
  if (channelName === "FB") col = 4;

  if (col === 0) {
    OWTR_alert_("未知的發布管道:" + channelName);
    return;
  }

  sheet.getRange(row, col).setValue("已發布 " + nowText);
  sheet.getRange(row, 8).setValue(new Date());

  OWTR_refreshDashboardForWeek_(weekId);
  OWTR_updateWeeklyIndex_();

  OWTR_alert_(channelName + " 已標記發布。");
}

function OWTR_ensurePublishLogRow_(weekId) {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const sheet = ss.getSheetByName(OWTR_CONFIG.SHEETS.PUBLISH_LOG);

  if (!sheet) return;

  const row = OWTR_findRowByWeekId_(sheet, weekId);
  if (row > 0) return;

  sheet.appendRow([
    weekId,
    "尚未發布",
    "尚未發布",
    "尚未發布",
    "尚未發布",
    "",
    "",
    new Date(),
  ]);
}

function OWTR_updatePublishLogDocUrl_(weekId, docUrl) {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const sheet = ss.getSheetByName(OWTR_CONFIG.SHEETS.PUBLISH_LOG);
  if (!sheet) {
    OWTR_alert_("找不到「03_發布紀錄」,請先執行「初始化平台」。");
    return;
  }

  OWTR_ensurePublishLogRow_(weekId);

  const row = OWTR_findRowByWeekId_(sheet, weekId);
  sheet.getRange(row, 6).setValue(docUrl);
  sheet.getRange(row, 8).setValue(new Date());
}

/**
 * =========================
 * 歷週索引
 * =========================
 */

function OWTR_updateWeeklyIndex() {
  OWTR_runAction_("更新歷週索引", function () {
    OWTR_updateWeeklyIndex_();
    OWTR_alert_("歷週索引已更新。");
  });
}

function OWTR_updateWeeklyIndex_() {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const indexSheet = ss.getSheetByName(OWTR_CONFIG.SHEETS.INDEX);
  const weeklySheet = ss.getSheetByName(OWTR_CONFIG.SHEETS.WEEKLY_DATA);
  const logSheet = ss.getSheetByName(OWTR_CONFIG.SHEETS.PUBLISH_LOG);

  if (!indexSheet || !weeklySheet || !logSheet) return;

  const lastRow = weeklySheet.getLastRow();
  indexSheet
    .getRange(2, 1, Math.max(indexSheet.getMaxRows() - 1, 1), 8)
    .clearContent()
    .clearFormat();
  OWTR_styleHeader_(indexSheet, 8);

  if (lastRow < 2) return;

  const weeklyValues = weeklySheet.getRange(2, 1, lastRow - 1, 10).getValues();
  const settings = OWTR_getSettings_();
  const classWebsiteUrl = settings["班級網站網址"] || "";
  const rows = [];
  const linkRows = [];

  weeklyValues.forEach(function (row) {
    const weekId = row[0];
    if (!weekId) return;

    const data = {
      weekId: weekId,
      dateRange: row[1],
      learningFocus: row[2],
      activities: row[3],
      highlights: row[4],
      reminders: row[5],
      parentTasks: row[6],
      photoFolderUrl: row[7],
      createdAt: row[8],
      updatedAt: row[9],
    };

    const logRow = OWTR_findRowByWeekId_(logSheet, weekId);
    let docUrl = "";
    let publishStatus = "尚未發布";
    let lastUpdated = data.updatedAt || "";

    if (logRow > 0) {
      const logValues = logSheet.getRange(logRow, 1, 1, 8).getValues()[0];
      docUrl = logValues[5];
      publishStatus = OWTR_buildPublishSummary_(
        logValues[1],
        logValues[2],
        logValues[3],
        logValues[4],
      );
      lastUpdated = logValues[7] || lastUpdated;
    }

    rows.push([
      weekId,
      data.dateRange,
      OWTR_makeWeeklySummary_(data),
      docUrl ? "開啟 Docs" : "尚未建立",
      data.photoFolderUrl ? "開啟照片資料夾" : "尚未建立",
      classWebsiteUrl ? "開啟班級網站" : "尚未設定",
      publishStatus,
      lastUpdated,
    ]);
    linkRows.push({
      docUrl: docUrl,
      photoFolderUrl: data.photoFolderUrl,
      classWebsiteUrl: classWebsiteUrl,
    });
  });

  if (rows.length > 0) {
    indexSheet.getRange(2, 1, rows.length, 8).setValues(rows);
    rows.forEach(function (row, index) {
      const sheetRow = index + 2;
      OWTR_setLinkCell_(indexSheet, sheetRow, 4, "開啟 Docs", linkRows[index].docUrl, "尚未建立");
      OWTR_setLinkCell_(
        indexSheet,
        sheetRow,
        5,
        "開啟照片資料夾",
        linkRows[index].photoFolderUrl,
        "尚未建立",
      );
      OWTR_setLinkCell_(
        indexSheet,
        sheetRow,
        6,
        "開啟班級網站",
        linkRows[index].classWebsiteUrl,
        "尚未設定",
      );
    });
  }
}

function OWTR_makeWeeklySummary_(data) {
  const source = [
    OWTR_stripPlaceholder_(data.learningFocus, "learningFocus"),
    OWTR_stripPlaceholder_(data.activities, "activities"),
    OWTR_stripPlaceholder_(data.highlights, "highlights"),
  ]
    .filter(function (text) {
      return text !== "";
    })
    .join(" / ");

  return OWTR_truncateText_(source || "尚未填寫摘要", 180);
}

function OWTR_buildPublishSummary_(websiteStatus, lineStatus, fbStatus, collaborationStatus) {
  return [
    "班網:" + (websiteStatus || "尚未發布"),
    "LINE:" + (lineStatus || "尚未發布"),
    "FB:" + (fbStatus || "尚未發布"),
    "協作:" + (collaborationStatus || "尚未發布"),
  ].join("\n");
}

/**
 * =========================
 * 中控台更新
 * =========================
 */

function OWTR_refreshDashboardForWeek_(weekId) {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const dashboard = ss.getSheetByName(OWTR_CONFIG.SHEETS.DASHBOARD);
  if (!dashboard) return;

  const data = weekId ? OWTR_getWeeklyDataByWeekId_(weekId) : null;
  const settings = OWTR_getSettings_();
  const classWebsiteUrl = settings["班級網站網址"] || "";
  const publishInfo = OWTR_getPublishInfo_(weekId);
  const hasCopies = OWTR_hasThreeChannelCopies_(weekId);

  if (!data) {
    dashboard.getRange("B4").setValue("");
    dashboard.getRange("B5").setValue("");
    OWTR_setStatusCell_(dashboard, "E4", OWTR_CONFIG.STATUS.NOT_CREATED);
    dashboard.getRange("D10").setValue("⚪ 尚未發布");
    dashboard.getRange("B16").setValue("尚未發布");
    OWTR_updateDashboardFlowSteps_(dashboard, null, publishInfo, hasCopies);
    OWTR_setLinkCell_(dashboard, 5, 5, "開啟 Google Docs", "", "尚未建立");
    OWTR_setLinkCell_(dashboard, 6, 2, "開啟班級網站", classWebsiteUrl, "尚未設定");
    OWTR_setLinkCell_(dashboard, 6, 5, "開啟照片資料夾", "", "尚未建立");
    return;
  }

  dashboard.getRange("B4").setValue(data.weekId);
  dashboard.getRange("B5").setValue(data.dateRange);

  const status = OWTR_getOverallStatus_(publishInfo, hasCopies);
  OWTR_setStatusCell_(dashboard, "E4", status);
  OWTR_setLinkCell_(dashboard, 5, 5, "開啟 Google Docs", publishInfo.docUrl, "尚未建立");
  OWTR_setLinkCell_(dashboard, 6, 2, "開啟班級網站", classWebsiteUrl, "尚未設定");
  OWTR_setLinkCell_(dashboard, 6, 5, "開啟照片資料夾", data.photoFolderUrl, "尚未建立");

  dashboard.getRange("B9").setValue(OWTR_CONFIG.STATUS.DRAFT);
  dashboard
    .getRange("D9")
    .setValue(hasCopies ? OWTR_CONFIG.STATUS.COPY_READY : "⚪ 尚未產生");
  dashboard
    .getRange("F9")
    .setValue(publishInfo.docUrl ? OWTR_CONFIG.STATUS.DOC_READY : "⚪ 尚未產生");
  dashboard
    .getRange("B10")
    .setValue(status === OWTR_CONFIG.STATUS.COMPLETED ? status : "⚪ 尚未完成");
  dashboard
    .getRange("D10")
    .setValue(
      OWTR_isPublished_(publishInfo.collaborationStatus)
        ? OWTR_CONFIG.STATUS.COLLAB_READY
        : "⚪ 尚未發布",
    );

  dashboard.getRange("B13").setValue(publishInfo.websiteStatus || "尚未發布");
  dashboard.getRange("B14").setValue(publishInfo.lineStatus || "尚未發布");
  dashboard.getRange("B15").setValue(publishInfo.fbStatus || "尚未發布");
  dashboard.getRange("B16").setValue(publishInfo.collaborationStatus || "尚未發布");
  OWTR_updateDashboardFlowSteps_(dashboard, data, publishInfo, hasCopies);
}

function OWTR_updateDashboardFlowSteps_(dashboard, data, publishInfo, hasCopies) {
  const hasWeek = Boolean(data);
  const fieldsDone = hasWeek && OWTR_getIncompleteFieldWarnings_(data).length === 0;
  const docAndPhotoDone = hasWeek && Boolean(publishInfo.docUrl) && Boolean(data.photoFolderUrl);
  const threeChannelDone =
    OWTR_isPublished_(publishInfo.websiteStatus) &&
    OWTR_isPublished_(publishInfo.lineStatus) &&
    OWTR_isPublished_(publishInfo.fbStatus);
  const collaborationDone = OWTR_isPublished_(publishInfo.collaborationStatus);
  const rows = [
    hasWeek,
    fieldsDone,
    hasCopies,
    docAndPhotoDone,
    threeChannelDone,
    collaborationDone,
  ].map(function (done) {
    return [done ? "✅ 已完成" : "⚪ 待處理"];
  });

  dashboard.getRange(19, 3, rows.length, 1).setValues(rows);
  dashboard
    .getRange("E19")
    .setValue(hasWeek ? OWTR_getNextActionText_(data, publishInfo, hasCopies) : "請先建立本週週報範本");
}

function OWTR_getOverallStatus_(publishInfo, hasCopies) {
  const isCompleted =
    OWTR_isPublished_(publishInfo.websiteStatus) &&
    OWTR_isPublished_(publishInfo.lineStatus) &&
    OWTR_isPublished_(publishInfo.fbStatus);

  if (isCompleted) return OWTR_CONFIG.STATUS.COMPLETED;
  if (OWTR_isPublished_(publishInfo.collaborationStatus)) {
    return OWTR_CONFIG.STATUS.COLLAB_READY;
  }
  if (publishInfo.docUrl) return OWTR_CONFIG.STATUS.DOC_READY;
  if (hasCopies) return OWTR_CONFIG.STATUS.COPY_READY;
  return OWTR_CONFIG.STATUS.DRAFT;
}

function OWTR_setStatusCell_(sheet, a1Notation, status) {
  const range = sheet.getRange(a1Notation);
  range.setValue(status).setFontWeight("bold").setHorizontalAlignment("center");

  const colorMap = {};
  colorMap[OWTR_CONFIG.STATUS.NOT_CREATED] = "#f3f4f6";
  colorMap[OWTR_CONFIG.STATUS.DRAFT] = "#fff7d6";
  colorMap[OWTR_CONFIG.STATUS.COPY_READY] = "#dcfce7";
  colorMap[OWTR_CONFIG.STATUS.DOC_READY] = "#dbeafe";
  colorMap[OWTR_CONFIG.STATUS.COLLAB_READY] = "#ede9fe";
  colorMap[OWTR_CONFIG.STATUS.COMPLETED] = "#d1fae5";

  range.setBackground(colorMap[status] || "#f3f4f6");
}

function OWTR_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 OWTR_getPublishInfo_(weekId) {
  const emptyInfo = {
    websiteStatus: "尚未發布",
    lineStatus: "尚未發布",
    fbStatus: "尚未發布",
    collaborationStatus: "尚未發布",
    docUrl: "",
  };
  if (!weekId) return emptyInfo;

  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const logSheet = ss.getSheetByName(OWTR_CONFIG.SHEETS.PUBLISH_LOG);
  if (!logSheet) return emptyInfo;

  const row = OWTR_findRowByWeekId_(logSheet, weekId);
  if (row <= 0) return emptyInfo;

  const values = logSheet.getRange(row, 1, 1, 8).getValues()[0];
  return {
    websiteStatus: values[1] || "尚未發布",
    lineStatus: values[2] || "尚未發布",
    fbStatus: values[3] || "尚未發布",
    collaborationStatus: values[4] || "尚未發布",
    docUrl: values[5] || "",
  };
}

function OWTR_hasThreeChannelCopies_(weekId) {
  if (!weekId) return false;

  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const outputSheet = ss.getSheetByName(OWTR_CONFIG.SHEETS.COPY_OUTPUT);
  if (!outputSheet) return false;

  return OWTR_findRowByWeekId_(outputSheet, weekId) > 0;
}

function OWTR_isPublished_(status) {
  return String(status || "").indexOf("已發布") === 0;
}

/**
 * =========================
 * 資料讀取工具
 * =========================
 */

function OWTR_getActiveWeekId_() {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const dashboard = ss.getSheetByName(OWTR_CONFIG.SHEETS.DASHBOARD);
  if (!dashboard) return "";

  return dashboard.getRange("B4").getValue() || dashboard.getRange("B3").getValue();
}

function OWTR_getLatestWeekId_() {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const sheet = ss.getSheetByName(OWTR_CONFIG.SHEETS.WEEKLY_DATA);
  if (!sheet || sheet.getLastRow() < 2) return "";

  return sheet.getRange(sheet.getLastRow(), 1).getValue();
}

function OWTR_getWeeklyDataByWeekId_(weekId) {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const sheet = ss.getSheetByName(OWTR_CONFIG.SHEETS.WEEKLY_DATA);
  if (!sheet) return null;

  const row = OWTR_findRowByWeekId_(sheet, weekId);
  if (row <= 0) return null;

  const values = sheet.getRange(row, 1, 1, 10).getValues()[0];

  return {
    weekId: values[0],
    dateRange: values[1],
    learningFocus: values[2],
    activities: values[3],
    highlights: values[4],
    reminders: values[5],
    parentTasks: values[6],
    photoFolderUrl: values[7],
    createdAt: values[8],
    updatedAt: values[9],
  };
}

function OWTR_getSettings_() {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const sheet = ss.getSheetByName(OWTR_CONFIG.SHEETS.SETTINGS);
  const settings = {};

  if (!sheet) return settings;

  const lastRow = sheet.getLastRow();
  if (lastRow < 2) return settings;

  const values = sheet.getRange(2, 1, lastRow - 1, 2).getValues();

  values.forEach(function (row) {
    const key = row[0];
    const value = row[1];
    if (key) settings[key] = value;
  });

  return settings;
}

function OWTR_findSettingsRow_(settingName) {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const sheet = ss.getSheetByName(OWTR_CONFIG.SHEETS.SETTINGS);
  if (!sheet) return -1;

  const lastRow = sheet.getLastRow();
  if (lastRow < 1) return -1;

  const values = sheet.getRange(1, 1, lastRow, 1).getValues();

  for (let i = 0; i < values.length; i++) {
    if (values[i][0] === settingName) {
      return i + 1;
    }
  }

  return -1;
}

function OWTR_findRowByWeekId_(sheet, weekId) {
  if (!sheet || !weekId) return -1;

  const lastRow = sheet.getLastRow();
  if (lastRow < 2) return -1;

  const values = sheet.getRange(2, 1, lastRow - 1, 1).getValues();

  for (let i = 0; i < values.length; i++) {
    if (values[i][0] === weekId) {
      return i + 2;
    }
  }

  return -1;
}

/**
 * =========================
 * 共用工具
 * =========================
 */

function OWTR_getOrCreateSheet_(ss, sheetName) {
  let sheet = ss.getSheetByName(sheetName);

  if (!sheet) {
    sheet = ss.insertSheet(sheetName);
  }

  return sheet;
}

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

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

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

function OWTR_compactText_(text) {
  if (!text) return "尚未填寫。";

  return String(text)
    .split("\n")
    .map(function (line) {
      return line.trim();
    })
    .filter(function (line) {
      return line !== "";
    })
    .join("\n");
}

function OWTR_stripPlaceholder_(text, key) {
  if (OWTR_isPlaceholderOrBlank_(key, text)) return "";
  return OWTR_compactText_(text);
}

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

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

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

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

function OWTR_runAction_(actionName, callback) {
  try {
    OWTR_ensureMvp03Structure_(SpreadsheetApp.getActiveSpreadsheet());
    callback();
  } catch (e) {
    Logger.log(e && e.stack ? e.stack : e);
    OWTR_alert_(
      actionName +
        "失敗。\n\n請確認已完成初始化、目前週次存在,且授權 Drive / Docs / Spreadsheet 權限。\n\n錯誤訊息:" +
        (e && e.message ? e.message : e),
    );
  }
}
提醒:請完整複製,不要只複製部分函式。若 Apps Script 要求授權,請依畫面完成授權流程。發布前仍建議由老師檢查內容與學生個資。

版本一致性提醒

本頁內嵌的 GAS 程式碼應與專案中的 .gs 原始檔保持一致。若日後修改 .gs,請同步更新此 HTML 程式碼頁,避免研習現場使用到舊版本。