function onOpen() { SpreadsheetApp.getUi() .createMenu('行政工作流') .addItem('初始化課程模板', 'setupTemplate') .addSeparator() .addItem('建立專案資料夾', 'createProjectWorkspace') .addItem('建立任務追蹤表', 'createTaskSheet') .addItem('建立日曆提醒', 'createCalendarReminders') .addItem('產生通知草稿', 'createNotificationDraft') .addToUi(); } function getConfigSheet() { const ss = SpreadsheetApp.getActive(); return ss.getSheetByName('專案設定') || ss.getSheets()[0]; } function setupTemplate() { const ss = SpreadsheetApp.getActive(); const sheet = ss.getSheetByName('專案設定') || ss.getActiveSheet(); sheet.setName('專案設定'); sheet.clear(); sheet.getRange('A1:B5').setValues([ ['欄位', '內容'], ['專案名稱', '校慶活動籌備'], ['專案日期', new Date('2026/09/15')], ['承辦單位', '學務處'], ['專案資料夾連結', '由 GAS 自動產生'] ]); sheet.getRange('A1:B1').setFontWeight('bold'); sheet.getRange('B3').setNumberFormat('yyyy/mm/dd'); sheet.autoResizeColumns(1, 2); createTaskSheet(); SpreadsheetApp.getUi().alert('已初始化課程模板,請先確認 B2:B4 後再建立專案資料夾。'); } function createProjectWorkspace() { const sheet = getConfigSheet(); const projectName = sheet.getRange('B2').getValue() || '行政專案'; const projectDate = sheet.getRange('B3').getValue() || new Date(); const unit = sheet.getRange('B4').getValue() || '承辦單位'; const existingFolderUrl = sheet.getRange('B5').getValue(); if (existingFolderUrl && existingFolderUrl !== '由 GAS 自動產生') { SpreadsheetApp.getUi().alert('此專案已建立資料夾,若要重新建立,請先清空 B5。'); return; } const yyyyMMdd = Utilities.formatDate(new Date(projectDate), Session.getScriptTimeZone(), 'yyyyMMdd'); const rootName = `${yyyyMMdd}_${projectName}_${unit}`; // 教學版直接建立在我的雲端硬碟根目錄;正式版可改用 ROOT_FOLDER_ID 指定根資料夾。 const rootFolder = DriveApp.createFolder(rootName); const subFolders = [ '01_原始資料', '02_工作文件', '03_名冊表單', '04_通知公告', '05_會議與協調', '06_成果結案', '99_封存與舊版' ]; subFolders.forEach(name => rootFolder.createFolder(name)); sheet.getRange('B5').setValue(rootFolder.getUrl()); SpreadsheetApp.getUi().alert('已建立專案資料夾:' + rootName); } function createTaskSheet() { const ss = SpreadsheetApp.getActive(); const taskSheet = ss.getSheetByName('任務追蹤') || ss.insertSheet('任務追蹤'); taskSheet.clear(); const headers = [ '任務名稱', '負責人', '期限', '狀態', '輸出文件', '提醒天數', '風險提醒', '備註', '日曆事件ID' ]; const rows = [ ['確認專案需求與對象', '承辦人', new Date('2026/09/01'), '待確認', '專案摘要', 3, '需確認主管期待', '', ''], ['整理參與名冊', '承辦人', new Date('2026/09/03'), '未開始', '名冊 Sheet', 3, '含個資,注意權限', '', ''], ['產生通知草稿', '承辦人', new Date('2026/09/05'), '未開始', '通知 Docs/Gmail 草稿', 1, '寄送前需人工確認', '', ''], ['彙整成果資料', '承辦人', new Date('2026/09/16'), '未開始', '成果資料夾', 0, '活動後容易漏', '', ''], ['完成結案紀錄', '承辦人', new Date('2026/09/20'), '未開始', '結案報告草稿', 3, '需補上實際成果', '', ''] ]; taskSheet.getRange(1, 1, 1, headers.length).setValues([headers]).setFontWeight('bold'); taskSheet.getRange(2, 1, rows.length, headers.length).setValues(rows); taskSheet.getRange(2, 3, rows.length, 1).setNumberFormat('yyyy/mm/dd'); taskSheet.getRange('D2:D').setDataValidation( SpreadsheetApp.newDataValidation() .requireValueInList(['未開始', '進行中', '待確認', '已完成', '暫停'], true) .build() ); taskSheet.autoResizeColumns(1, headers.length); } function createCalendarReminders() { const ss = SpreadsheetApp.getActive(); const mainSheet = getConfigSheet(); const projectName = mainSheet.getRange('B2').getValue() || '行政專案'; const sheet = ss.getSheetByName('任務追蹤'); if (!sheet) { SpreadsheetApp.getUi().alert('找不到「任務追蹤」工作表'); return; } const rows = sheet.getDataRange().getValues(); const calendar = CalendarApp.getDefaultCalendar(); let created = 0; let skipped = 0; rows.slice(1).forEach((row, index) => { const taskName = row[0]; const owner = row[1]; const dueDate = row[2]; const status = row[3]; const reminderDays = Number(row[5] || 0); const risk = row[6]; const note = row[7]; const eventId = row[8]; if (!taskName || status === '已完成' || !(dueDate instanceof Date) || eventId) { skipped++; return; } const reminderDate = new Date(dueDate); reminderDate.setDate(reminderDate.getDate() - reminderDays); const title = `[行政提醒] ${taskName}`; const description = [ `專案名稱:${projectName}`, `負責人:${owner || '未填'}`, `原始期限:${Utilities.formatDate(dueDate, Session.getScriptTimeZone(), 'yyyy/MM/dd')}`, `提醒天數:${reminderDays}`, `風險提醒:${risk || '無'}`, `備註:${note || '無'}` ].join('\n'); const event = calendar.createAllDayEvent(title, reminderDate, { description }); sheet.getRange(index + 2, 9).setValue(event.getId()); created++; }); SpreadsheetApp.getUi().alert(`已建立 ${created} 個日曆提醒,略過 ${skipped} 筆。`); } function createNotificationDraft() { const sheet = getConfigSheet(); const projectName = sheet.getRange('B2').getValue() || '行政專案'; const owner = sheet.getRange('B4').getValue() || '承辦人'; const folderUrl = sheet.getRange('B5').getValue() || ''; const subject = `[草稿待確認] ${projectName} 行政工作流建案通知`; const body = [ `${owner} 您好:`, '', `「${projectName}」的行政專案資料夾與追蹤表已建立。`, folderUrl ? `專案資料夾:${folderUrl}` : '專案資料夾:請補上連結', '', '請先確認:', '1. 參與對象與名冊是否正確', '2. 期限與負責人是否需要調整', '3. 是否包含個資或需限制權限的資料', '4. 正式通知內容是否需主管或處室確認', '', '本信為 GAS 產生的草稿,寄出前請人工確認。' ].join('\n'); // 教學版預設寄給使用者自己,讓承辦人先檢查通知內容,不直接對外寄送。 const recipient = Session.getActiveUser().getEmail(); GmailApp.createDraft(recipient, subject, body); SpreadsheetApp.getUi().alert('已建立 Gmail 草稿,請到 Gmail 草稿匣確認後再寄出'); }