/** * 行政工作流有感示範版 * * 用途: * 讓學員在上半場立即看見 Google Sheets、Drive、Calendar 與 Gmail 草稿 * 可以被 Google Apps Script 串成一個行政工作流原型。 * * 使用階段: * 上半場 basic GAS 有感示範。 * * 使用方式: * 1. 在 Google Sheets 開啟「擴充功能 > Apps Script」。 * 2. 貼上本檔內容後儲存。 * 3. 重新整理試算表,使用「行政工作流」自訂選單。 * * 安全原則: * - Gmail 只建立草稿,不自動寄出。 * - Drive 只建立資料夾,不自動開放權限。 * - 大量修改 Sheets 或建立外部資源前,先跳出確認提醒。 */ const BASIC_FOLDER_PLACEHOLDER = '由 GAS 自動產生'; /** * 建立試算表自訂選單。 * 讀取:無。 * 建立:Google Sheets 的「行政工作流」選單。 * 回寫:無。 */ 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 confirmBasicAction_(message) { const ui = SpreadsheetApp.getUi(); const result = ui.alert('行政工作流提醒', message, ui.ButtonSet.OK_CANCEL); return result === ui.Button.OK; } /** * 初始化基礎模板。 * 讀取:目前試算表。 * 建立:專案設定表與任務追蹤表。 * 回寫:B2:B5 與任務追蹤 A:I 範例資料。 */ function setupTemplate() { if (!confirmBasicAction_('這會清空「專案設定」並重建「任務追蹤」範例資料。是否繼續?')) { return; } const ss = SpreadsheetApp.getActive(); const sheet = ss.getSheetByName('專案設定') || ss.getActiveSheet(); sheet.setName('專案設定'); sheet.clear(); sheet.getRange('A1:B5').setValues([ ['欄位', '內容'], ['專案名稱', '校慶活動籌備'], ['專案日期', new Date('2026/09/15')], ['承辦單位', '學務處'], ['專案資料夾連結', BASIC_FOLDER_PLACEHOLDER] ]); sheet.getRange('A1:B1').setFontWeight('bold'); sheet.getRange('B3').setNumberFormat('yyyy/mm/dd'); sheet.autoResizeColumns(1, 2); resetBasicTaskSheet_(); SpreadsheetApp.getUi().alert('已初始化基礎模板,請先確認 B2:B4 後再建立專案資料夾。'); } /** * 建立 Drive 專案資料夾。 * 讀取:專案設定 B2 專案名稱、B3 專案日期、B4 承辦單位、B5 既有資料夾連結。 * 建立:Drive 專案資料夾與標準子資料夾。 * 回寫:B5 專案資料夾連結;不會自動開放 Drive 權限。 */ function createProjectWorkspace() { if (!confirmBasicAction_('這會在我的雲端硬碟根目錄建立專案資料夾與子資料夾,但不會自動開放權限。是否繼續?')) { return; } 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 !== BASIC_FOLDER_PLACEHOLDER) { SpreadsheetApp.getUi().alert('此專案已建立資料夾。若要重新建立,請先清空 B5。'); return; } const yyyyMMdd = Utilities.formatDate(new Date(projectDate), Session.getScriptTimeZone(), 'yyyyMMdd'); const rootName = `${yyyyMMdd}_${projectName}_${unit}`; const rootFolder = DriveApp.createFolder(rootName); getBasicSubFolderNames_().forEach(name => rootFolder.createFolder(name)); sheet.getRange('B5').setValue(rootFolder.getUrl()); SpreadsheetApp.getUi().alert(`已建立專案資料夾:${rootName}\n\n提醒:本程式不會自動開放 Drive 權限,請由承辦人確認後手動分享。`); } /** * 建立任務追蹤表。 * 讀取:無。 * 建立:任務追蹤工作表。 * 回寫:任務追蹤 A:I 欄位與範例任務。 */ function createTaskSheet() { if (!confirmBasicAction_('這會清空並重建「任務追蹤」工作表。是否繼續?')) { return; } resetBasicTaskSheet_(); SpreadsheetApp.getUi().alert('已建立任務追蹤表。'); } /** * 建立 Calendar 提醒。 * 讀取:任務追蹤 A 任務名稱、C 期限、D 狀態、F 提醒天數、I 日曆事件ID。 * 建立:Google Calendar 全天事件。 * 回寫:任務追蹤 I 欄日曆事件 ID,用來避免重複建立事件。 */ function createCalendarReminders() { if (!confirmBasicAction_('這會依「任務追蹤」中的期限建立 Calendar 提醒。已有日曆事件ID的列會略過,避免重複建立。是否繼續?')) { return; } 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 || '未填'}`, `原始期限:${formatBasicDate_(dueDate)}`, `提醒天數:${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} 筆。`); } /** * 產生 Gmail 通知草稿。 * 讀取:專案設定 B2 專案名稱、B4 承辦單位、B5 資料夾連結。 * 建立:寄給自己的 Gmail 草稿。 * 回寫:無;不會自動寄出正式 Email。 */ function createNotificationDraft() { if (!confirmBasicAction_('這會建立一封寄給自己的 Gmail 草稿,不會自動寄出。是否繼續?')) { return; } 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 草稿匣確認後再寄出。'); } function resetBasicTaskSheet_() { 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 getBasicSubFolderNames_() { return [ '01_原始資料', '02_工作文件', '03_名冊表單', '04_通知公告', '05_會議與協調', '06_成果結案', '99_封存與舊版' ]; } function formatBasicDate_(date) { return Utilities.formatDate(date, Session.getScriptTimeZone(), 'yyyy/MM/dd'); }