一、第二堂課課程定位
第二堂課主題為「活動型行政專案工作流:從活動建案到流程展開」。
第一堂課處理「公文如何變成專案」,第二堂課則進一步處理「專案如何展開成流程」。因此,你在第二堂課要看的不是報名系統,也不只是 Google 表單操作,而是活動型行政專案的生命週期與流程控制。
你將能以「五年級校外教學」為案例,拆解出規劃、通知、收件、確認、執行、結案六階段,並用 Apps Script 讓校外教學建案自動產生資料夾、文件範本、日曆節點與總表狀態。
二、第一堂課 vs 第二堂課:專案管理能力差異
兩堂課除了主題不同,更重要的是專案管理能力有明確進階。
| 面向 | 第一堂課 | 第二堂課 |
|---|---|---|
| 主題 | 公文專案化 | 活動型專案流程展開 |
| 專案管理重點 | 建案與結構化 | 生命週期與節點管理 |
| 核心能力 | 把行政任務變成專案物件 | 把行政專案拆成多階段流程 |
| 管理對象 | 單一行政案件 | 多階段活動流程 |
| 專案工具角色 | 建立專案骨架 | 展開專案流程 |
| 關鍵字 | 建案、分類、狀態、資料夾、責任歸屬 | 生命週期、里程碑、WBS、風險節點、利害關係人、交付成果 |
第一堂課:建案能力
你會學會判斷行政任務是否需要成案,並建立基本資料、承辦責任、期限、狀態與資料夾。
第二堂課:展案能力
你會學會把活動專案拆成階段、任務包、日期節點、資料流、風險節點與交付成果。
三、第二堂課 120 分鐘課程流程
| 時間 | 單元 | 內容重點 | 學習驗證 |
|---|---|---|---|
| 0–10 分 | 回顧第一堂課 | 行政任務如何從公文變成專案;任務流、資訊流、責任流。 | 能說出第一堂課的建案邏輯。 |
| 10–25 分 | 校外教學行政專案特性與學理基礎 | 多階段、多對象、多資料、多風險;銜接生命週期、WBS、里程碑、RACI、風險登錄表。 | 能舉出校外教學最常卡住的節點,例如同意書、保險、交通、名冊或成果資料,並說明它屬於哪一類專案管理問題。 |
| 25–40 分 | 校外教學生命週期拆解 | 規劃、通知、收件、確認、執行、結案。 | 能拆出至少 3 個時間節點、3 種資料、2 個責任角色。 |
| 40–55 分 | 校外教學資料模型設計 | 活動建案表單欄位如何驅動後續流程。 | 能判斷哪些欄位是必要欄位。 |
| 55–70 分 | 校外教學資料夾模板設計 | 建立活動型專案的固定資料結構。 | 能說明每個子資料夾的行政用途。 |
| 70–90 分 | 工具對應與流程示範 | Form、Sheets、Drive、Calendar、Docs、Apps Script 如何串接。 | 能理解表單送出後的自動流程。 |
| 90–110 分 | 實作與驗證 | 送出校外教學建案,產生資料夾、文件、日曆與總表狀態。 | 提交資料夾連結、總表截圖或日曆事件。 |
| 110–120 分 | 銜接第三堂課 | 活動完成後,資料如何轉成成果報告、AI 檢核與組織記憶。 | 理解第三堂課會處理專案結案與成果整理。 |
四、第二堂課新增的專案管理內容
1. 專案生命週期
校外教學從規劃、通知、收件、確認、執行到結案,是連續流程,不是只有出發當天。
2. 里程碑管理
同意書回收期限、保險名冊確認日、行前通知日、出發日、成果截止日都是校外教學的重要里程碑。
3. 工作分解結構
將校外教學拆成工作包,例如家長通知、同意書回收、保險名冊、交通安排、帶隊分工、照片、成果與核銷。
4. 風險節點管理
找出最容易出錯的節點,例如同意書未收齊、保險名冊版本不一致、車輛分配錯誤、成果照片缺漏。
5. 利害關係人管理
學生、家長、導師、帶隊教師、行政、主管、交通或場館窗口需要不同資訊。
6. 資料版本一致性
參加名冊、保險名冊、車輛名冊、帶隊分組與成果人數應該來自一致的資料來源。
7. 交付成果管理
每個階段都要有產出,例如校外教學計畫、家長通知、同意書統計、保險名冊、車輛名冊、照片與成果報告。
8. 結案與交接
校外教學完成後,要留下成果、檢討、核銷與下一屆可複製的模板。
校外教學生命週期模型
| 階段 | 核心問題 | 主要資料 | 主要產出 |
|---|---|---|---|
| 規劃 | 校外教學去哪裡?哪個年級參加?何時出發與返回? | 公文、計畫、預算、場館、交通 | 校外教學計畫 |
| 通知 | 家長、導師、帶隊教師與行政需要知道什麼? | 校外教學資訊、集合時間、注意事項 | 公告、家長通知 |
| 收件 | 要收同意書與特殊需求資料?誰還沒交? | 同意書、特殊需求、緊急聯絡資料 | 回收資料 |
| 確認 | 資料是否完整?名冊是否正確? | 參加名冊、保險名冊、交通車次、帶隊分組 | 確認名冊與分車表 |
| 執行 | 出發當天如何點名、分車與回報? | 點名、照片、突發狀況紀錄 | 校外教學紀錄 |
| 結案 | 要回報什麼?下次如何接手? | 回饋、照片、核銷、檢討、交接 | 成果與交接 |
活動型專案管理學理補充
校外教學不是單一活動,而是一個具有明確目標、有限時間、跨角色協作、交付成果與風險限制的行政專案。從專案管理角度看,它同時包含範疇管理、時程管理、利害關係人管理、風險管理、溝通管理與知識管理。第二堂課的 Apps Script 不是為了取代這些判斷,而是把可標準化、可追蹤、可保存的部分系統化。
| 學理概念 | 核心意義 | 校外教學對應 | 本堂課如何落地 |
|---|---|---|---|
| 專案生命週期 | 專案通常經過啟動、規劃、執行、監控、結案;活動型行政也需要從「要辦」走到「可交接」。 | 校外教學從核定、計畫、通知、收件、出發、成果與核銷一路推進。 | 用表單建案後,自動建立資料夾、文件與日曆節點,讓每個階段都有可追蹤位置。 |
| 工作分解結構 WBS | 把大型任務拆成可執行、可分工、可檢查的工作包。 | 家長通知、同意書回收、保險名冊、交通分車、帶隊分工、照片成果、經費核銷。 | 以 8 個標準子資料夾對應工作包,降低資料散落與交接困難。 |
| 里程碑管理 | 里程碑不是待辦細節,而是會影響後續工作的關鍵節點。 | 同意書回收、保險名冊確認、行前通知、出發日、成果截止。 | 用 Calendar 事件把關鍵日期顯性化,避免只靠承辦人記憶。 |
| RACI 責任矩陣 | 釐清 Responsible 執行、Accountable 負責、Consulted 諮詢、Informed 被通知。 | 學務組長執行建案,主任或校長負責核定,導師與總務協作,家長與學生接收通知。 | 在建案資料中保留承辦人、協辦人與參加對象,後續可延伸成責任分工表。 |
| 利害關係人管理 | 不同角色需要不同資訊、不同時間點與不同溝通方式。 | 家長需要通知與同意書,導師需要名冊與分組,行政需要核銷與成果,交通窗口需要人數與時間。 | 用文件範本與資料夾分類,把資訊依用途放到不同位置。 |
| 風險登錄表 | 先列出可能出錯的事件,記錄影響、機率、處理方式與責任人。 | 同意書未收齊、保險資料錯誤、車輛不足、天候變化、學生臨時身體狀況。 | 課堂先用資料夾與提醒處理常見風險;進階版可把風險清單放入總控表。 |
| 單一事實來源 | 同一份資料不應在多處各自更新,否則會出現名冊版本不一致。 | 參加名冊、保險名冊、分車表與成果人數應該回到同一份總控資料查核。 | 活動專案總控表作為索引,所有資料夾與文件連結都回寫到同一列紀錄。 |
| PDCA 與組織記憶 | 行政專案不只完成當次任務,也要留下下一次可改進與複製的資料。 | 校外教學結案後留下成果照片、檢討、核銷資料與下次注意事項。 | 成果報告與結案交接資料夾保留記錄,銜接第三堂課的成果整理與 AI 檢核。 |
五、校外教學資料模型與資料夾模板
校外教學建案必要欄位
| 欄位 | 用途 |
|---|---|
| 活動名稱 | 固定以「五年級校外教學」作為課堂主案例,建立資料夾與文件命名。 |
| 活動類型 | 課堂填「校外教學」,用來標示流程模板與活動性質。 |
| 承辦人/協辦人 | 責任歸屬與協作管理。 |
| 活動日期 | 建立活動日事件。 |
| 報名截止日(校外教學課堂中作為同意書回收日) | 建立收件提醒。 |
| 行前通知日 | 建立活動前提醒。 |
| 成果截止日 | 建立成果整理提醒。 |
| 參加對象 | 例如五年級學生、五年級導師與帶隊教師,會寫入通知與成果文件。 |
| 預估人數 | 用於交通、保險、分組與成果統計。 |
| 是否需要同意書/保險/交通/核銷/成果 | 校外教學通常都需要,課堂用來示範行政需求如何寫入文件範本。 |
校外教學專案資料夾模板
01_校外教學計畫與公文 02_家長通知與同意書 03_回收資料與名冊 04_保險與交通 05_活動照片與紀錄 06_回饋與成果 07_經費與核銷 08_結案檢討與交接
學習驗證任務
| 驗證項目 | 最低完成標準 |
|---|---|
| 校外教學生命週期表 | 至少有 6 階段中的 4 階段。 |
| 時間節點 | 至少 3 個日期節點。 |
| 資料類型 | 至少 3 種校外教學資料,例如同意書、保險名冊、交通車次、照片或成果。 |
| 責任角色 | 至少 2 個責任角色。 |
| 結案成果 | 至少 1 個成果或交接文件。 |
六、第二堂課 Apps Script 規劃
第二堂課的 Script 是「校外教學建案引擎」,不是完整報名系統。它的任務是將五年級校外教學的表單資料,自動展開成資料夾、文件、日曆節點與總表追蹤狀態。
主要流程
功能分級
| 層級 | 功能 | 學習用途 |
|---|---|---|
| 活動安裝器主線 | 自動建立校外教學建案表、回應表、總控表、文件範本、觸發器、校外教學資料夾、文件與 Calendar 節點。 | 第二堂正式實作流程,降低手動設定錯誤。 |
| 備用短版 | 只建立校外教學主資料夾、8 個子資料夾、寫回連結與狀態。 | 現場無法使用安裝器時,保留最低成功版本。 |
| 課後進階版 | 依校外教學需求加入同意書追蹤、保險名冊檢核、交通分車表與資料缺漏檢查。 | 課後延伸或第三、四堂課銜接。 |
安裝器參數與預設值
key,value ROOT_FOLDER_ID,校外教學專案根資料夾ID CONTROL_SHEET_ID,可留空讓 setup 自動建立 CALENDAR_ID,primary SCHOOL_NAME,範例國小 ADMIN_EMAIL,錯誤通知信箱,可留空 TIMEZONE,Asia/Taipei ACTIVITY_TEMPLATE_DOCS,setup 自動建立校外教學計畫、家長通知、成果報告三份範本
表單回應欄位建議
時間戳記 活動名稱 活動類型 承辦人 協辦人 活動日期 報名截止日 行前通知日 成果截止日 參加對象 預估人數 是否需要同意書 是否需要保險資料 是否需要交通安排 是否需要經費核銷 是否需要成果報告 備註
總表右側追加欄位建議
活動編號 專案狀態 活動資料夾連結 校外教學計畫文件 家長通知文件 成果報告文件 報名截止事件(同意書回收) 行前通知事件 活動日事件 成果截止事件 日曆建立狀態 結案狀態 錯誤訊息
七、專案設置:使用安裝器前要先準備什麼
第二堂課的重點是讓活動資料能穩定展開成專案工作流。使用安裝器前,先把 Google Workspace 的承載位置、日曆、測試案例與權限確認好,後續 setup 才會一次成功。
專案設置清單
| 項目 | 操作內容 | 完成檢查 |
|---|---|---|
| Google 帳號 | 使用可操作 Drive、Forms、Sheets、Docs、Calendar、Apps Script 的帳號登入。建議課堂先用同一個帳號完成所有操作。 | 能開啟 drive.google.com、script.google.com,並能建立文件。 |
| Drive 總資料夾 | 在 Drive 建立「校外教學行政專案_課堂練習」。這是第二堂所有表單、回應表、總控表、文件範本與校外教學資料夾的根目錄。 | 複製資料夾網址或 /folders/ 後面的資料夾 ID。 |
| Calendar | 先決定提醒要寫入哪個日曆。課堂可先使用主要日曆 primary,回校後再換成處室共用日曆 ID。 | 不確定 Calendar ID 時,安裝器欄位填 primary。 |
| 活動專案總控表 | 可先不建立,讓 setupActivityWorkflow 自動建立;若已有既有總控表,再貼上 Google Sheet ID。 | 知道本次要用「自動建立」或「沿用既有表」。 |
| Apps Script 專案 | 到 script.google.com 建立新專案,專案名稱可設為「第二堂課_活動型行政專案工作流」。不要和第一堂課的 Apps Script 混在同一個專案。 | 可看到空白的 Code.gs,準備貼上安裝器產生的程式碼。 |
| 測試活動案例 | 先準備一筆「五年級校外教學」課堂測試資料。至少要有活動名稱、活動類型、承辦人、承辦人 Email、活動日期、報名截止日、行前通知日與成果截止日;其中「報名截止日」在本案例當作同意書回收期限。 | 送出活動建案表時不用臨場想欄位內容。 |
建議的 Drive 結構
校外教學行政專案_課堂練習/ ├─ 01_活動建案表/ ├─ 02_活動專案總控表/ ├─ 03_活動專案資料夾/ ├─ 04_活動文件範本/ └─ 06_Apps Script/
以上資料夾會由 setupActivityWorkflow 自動建立;使用前只需要先建立最上層的「校外教學行政專案_課堂練習」。
課堂測試資料範例
| 欄位 | 範例值 | 用途 |
|---|---|---|
| 活動名稱 | 五年級校外教學 | 建立資料夾與文件名稱。 |
| 活動類型 | 校外教學 | 寫入總控表與文件範本。 |
| 承辦人 / Email | 學務組長 / 可收信信箱 | 寫入文件與錯誤追蹤。 |
| 活動日期 | 2026/10/16 或課堂指定的未來日期 | 建立活動日 Calendar 節點。 |
| 同意書回收日、行前通知日、成果截止日 | 依序選在活動日前兩週、活動前三天、活動後一週 | 驗證多個 Calendar 節點是否建立。 |
| 是否需要同意書、保險、交通、核銷、成果 | 練習時可全部選「是」 | 寫入校外教學計畫、通知與成果報告範本。 |
八、安裝步驟:使用活動 Apps Script 安裝器
第二堂課建議優先使用活動安裝器,不再手動建立 Google Form、回應試算表、總控表與觸發器。安裝器會產生已帶入 Drive、Calendar、學校名稱與管理者 Email 的 Apps Script 程式碼。
| 步驟 | 操作內容 | 完成檢查 |
|---|---|---|
| 1 | 確認已完成上一節的專案設置,包含 Drive 總資料夾、Calendar 選擇、Apps Script 新專案與測試活動案例。 | 手上已有 Drive 資料夾 ID,並知道 Calendar 要填 primary 或共用日曆 ID。 |
| 2 | 開啟活動安裝器,填入 Drive 總資料夾 ID、活動專案總控表 ID、Calendar ID、學校名稱與管理者 Email。總控表 ID 可留空。 | 能按下「產生活動 Apps Script 程式碼」。 |
| 3 | 使用「複製程式碼」或「下載 .gs 程式碼」。這份程式碼已經帶入本次專案設置參數。 | 產生結果中可看到 setupActivityWorkflow 與 onActivityFormSubmit。 |
| 4 | 到 script.google.com 的新專案,把安裝器產生的程式碼貼進 Code.gs,儲存專案。 | 在 Apps Script 函式清單中可看到 testDriveFolder、testCalendar、setupActivityWorkflow、onActivityFormSubmit。 |
| 5 | 先執行 testDriveFolder,完成授權並確認 Drive 資料夾 ID 正確;若要檢查日曆,再執行 testCalendar。 | 執行紀錄顯示 Drive 資料夾名稱;Calendar 測試顯示日曆名稱。 |
| 6 | 執行 setupActivityWorkflow。 | 系統建立活動建案表、活動建案表_表單回應、活動專案總控表、活動文件範本與表單送出觸發器。 |
| 7 | 打開 Apps Script 執行紀錄,複製「活動建案表填寫網址」。 | 能開啟 Google Form 填寫頁。 |
| 8 | 用上一節準備的五年級校外教學測試資料送出一筆活動建案。 | 活動專案資料夾、8 個子資料夾、3 份文件、Calendar 節點與總控表紀錄都有產生。 |
安裝器會自動建立的項目
| 階段 | 產生項目 | 學習用途 |
|---|---|---|
| 執行 setup 後 | 活動建案表、回應試算表、活動專案總控表、校外教學計畫範本、家長通知範本、成果報告範本、表單送出觸發器。 | 把手動建表單與觸發器的時間,轉回活動流程設計與驗證。 |
| 送出活動建案表後 | 活動專案資料夾、8 個標準子資料夾、3 份套好欄位的文件、Calendar 日期節點、回應表右側狀態欄、總控表紀錄。 | 你會看到活動資料如何被展開成可追蹤的行政流程。 |
| Calendar 發生錯誤時 | 資料夾與文件仍會建立,回應表右側會顯示日曆錯誤訊息。 | 降低現場因 Calendar 權限或 ID 錯誤導致整個練習失敗的機率。 |
安裝後驗收順序
| 驗收項目 | 在哪裡看 | 成功標準 |
|---|---|---|
| 活動建案表 | Apps Script 執行紀錄中的填寫網址。 | 表單可開啟,且包含活動名稱、活動類型、承辦人、活動日期與行政需求欄位。 |
| Drive 結構 | Drive 總資料夾。 | 看到 01_活動建案表、02_活動專案總控表、03_活動專案資料夾、04_活動文件範本。 |
| 活動資料夾 | 03_活動專案資料夾。 | 送出測試資料後,出現以活動編號、日期與「五年級校外教學」命名的資料夾。 |
| 文件範本套印 | 活動資料夾中的校外教學計畫、家長通知、成果報告。 | 文件內的 {{活動名稱}}、{{活動日期}}、{{參加對象}} 等標籤已被五年級校外教學資料替換。 |
| 總控表紀錄 | 活動專案總控表。 | 新增一列校外教學紀錄,包含活動編號、狀態、資料夾連結與文件連結。 |
| Calendar 節點 | 指定 Calendar。 | 可看到校外教學活動日、同意書回收、行前通知或成果截止事件;若 Calendar 權限不足,回應表右側會留下錯誤訊息。 |
testDriveFolder、testCalendar、setupActivityWorkflow。setup 成功後,日常使用只需要填「活動建案表」。九、備用:課堂短版程式碼
此版本只保留為現場備援。若無法開啟活動安裝器,才使用這段短版程式確認最低限度的自動建案效果;正式流程仍以活動安裝器為主。
/**
* 第二堂課 Apps Script|備用短版
* 功能:表單送出後,自動建立校外教學主資料夾、8 個子資料夾,並寫回資料夾連結與狀態。
*
* 使用前請修改:
* 1. ROOT_FOLDER_ID
* 2. 活動名稱與活動日期所在欄位位置
* 3. 寫回欄位位置
*/
function onFormSubmit(e) {
const ROOT_FOLDER_ID = '請貼上校外教學專案根資料夾ID';
const TIMEZONE = 'Asia/Taipei';
const root = DriveApp.getFolderById(ROOT_FOLDER_ID);
const sheet = e.range.getSheet();
const row = e.range.getRow();
// 假設欄位順序:
// A 時間戳記、B 活動名稱、C 活動類型、D 承辦人、E 協辦人、F 活動日期
const activityName = sheet.getRange(row, 2).getValue();
const activityDate = sheet.getRange(row, 6).getValue();
const dateText = Utilities.formatDate(new Date(activityDate), TIMEZONE, 'yyyy-MM-dd');
const folderName = `${dateText}_${activityName}`;
const projectFolder = root.createFolder(folderName);
const subFolders = [
'01_校外教學計畫與公文',
'02_家長通知與同意書',
'03_回收資料與名冊',
'04_保險與交通',
'05_活動照片與紀錄',
'06_回饋與成果',
'07_經費與核銷',
'08_結案檢討與交接'
];
subFolders.forEach(name => projectFolder.createFolder(name));
// 建議在表單回應表右側預留欄位:
// R 活動資料夾連結、S 專案狀態、T 錯誤訊息
sheet.getRange(row, 18).setValue(projectFolder.getUrl());
sheet.getRange(row, 19).setValue('規劃中');
sheet.getRange(row, 20).setValue('');
}
十、課後參考:標準版程式邏輯
這段保留為課後閱讀與除錯參考。正式課堂操作請使用上一節的活動安裝器,由 setupActivityWorkflow 自動建立表單、總控表、文件範本與觸發器。
/**
* 第二堂課 Apps Script|課後參考標準邏輯
* 功能:
* 1. 表單送出後建立校外教學主資料夾與子資料夾
* 2. 複製校外教學計畫、家長通知、成果報告範本
* 3. 替換 Google Docs 內的 {{標籤}}
* 4. 建立報名截止、行前通知、活動日、成果截止 Calendar 事件
* 5. 寫回總表連結與狀態
*/
function onFormSubmit(e) {
const config = getConfig_();
const sheet = e.range.getSheet();
const row = e.range.getRow();
try {
const data = getRowData_(sheet, row, config);
const activityId = createActivityId_(data, config);
const folders = createActivityFolders_(data, config, activityId);
const docs = createActivityDocs_(data, config, folders);
const events = createActivityCalendarEvents_(data, config, folders.root.getUrl());
updateActivityRow_(sheet, row, {
activityId,
status: '規劃中',
folderUrl: folders.root.getUrl(),
planDocUrl: docs.planUrl || '',
noticeDocUrl: docs.noticeUrl || '',
reportDocUrl: docs.reportUrl || '',
registrationEventUrl: events.registrationUrl || '',
preNoticeEventUrl: events.preNoticeUrl || '',
activityEventUrl: events.activityUrl || '',
reportEventUrl: events.reportUrl || '',
calendarStatus: '已建立',
closeStatus: '未結案',
errorMessage: ''
}, config);
} catch (err) {
updateActivityRow_(sheet, row, {
errorMessage: err.message
}, config);
}
}
function getConfig_() {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sheet = ss.getSheetByName('設定');
if (!sheet) throw new Error('找不到「設定」工作表');
const values = sheet.getDataRange().getValues();
const config = {};
values.forEach(row => {
const key = row[0];
const value = row[1];
if (key) config[key] = value;
});
config.TIMEZONE = config.TIMEZONE || 'Asia/Taipei';
config.CALENDAR_ID = config.CALENDAR_ID || 'primary';
config.COL_ACTIVITY_ID = Number(config.COL_ACTIVITY_ID || 18);
config.COL_STATUS = Number(config.COL_STATUS || 19);
config.COL_FOLDER_URL = Number(config.COL_FOLDER_URL || 20);
config.COL_PLAN_DOC_URL = Number(config.COL_PLAN_DOC_URL || 21);
config.COL_NOTICE_DOC_URL = Number(config.COL_NOTICE_DOC_URL || 22);
config.COL_REPORT_DOC_URL = Number(config.COL_REPORT_DOC_URL || 23);
config.COL_REG_EVENT_URL = Number(config.COL_REG_EVENT_URL || 24);
config.COL_PRE_NOTICE_EVENT_URL = Number(config.COL_PRE_NOTICE_EVENT_URL || 25);
config.COL_ACTIVITY_EVENT_URL = Number(config.COL_ACTIVITY_EVENT_URL || 26);
config.COL_REPORT_EVENT_URL = Number(config.COL_REPORT_EVENT_URL || 27);
config.COL_CALENDAR_STATUS = Number(config.COL_CALENDAR_STATUS || 28);
config.COL_CLOSE_STATUS = Number(config.COL_CLOSE_STATUS || 29);
config.COL_ERROR = Number(config.COL_ERROR || 30);
return config;
}
function getRowData_(sheet, row, config) {
return {
timestamp: sheet.getRange(row, 1).getValue(),
activityName: sheet.getRange(row, 2).getValue(),
activityType: sheet.getRange(row, 3).getValue(),
owner: sheet.getRange(row, 4).getValue(),
coOwner: sheet.getRange(row, 5).getValue(),
activityDate: sheet.getRange(row, 6).getValue(),
registrationDeadline: sheet.getRange(row, 7).getValue(),
preNoticeDate: sheet.getRange(row, 8).getValue(),
reportDeadline: sheet.getRange(row, 9).getValue(),
participants: sheet.getRange(row, 10).getValue(),
estimatedNumber: sheet.getRange(row, 11).getValue(),
needConsent: sheet.getRange(row, 12).getValue(),
needInsurance: sheet.getRange(row, 13).getValue(),
needTransportation: sheet.getRange(row, 14).getValue(),
needReimbursement: sheet.getRange(row, 15).getValue(),
needReport: sheet.getRange(row, 16).getValue(),
note: sheet.getRange(row, 17).getValue()
};
}
function createActivityId_(data, config) {
const dateText = Utilities.formatDate(new Date(data.activityDate), config.TIMEZONE, 'yyyyMMdd');
const safeName = String(data.activityName).replace(/[\\/:*?"<>|]/g, '');
return `ACT-${dateText}-${safeName}`;
}
function createActivityFolders_(data, config, activityId) {
if (!config.ROOT_FOLDER_ID) throw new Error('設定表缺少 ROOT_FOLDER_ID');
const root = DriveApp.getFolderById(config.ROOT_FOLDER_ID);
const dateText = Utilities.formatDate(new Date(data.activityDate), config.TIMEZONE, 'yyyy-MM-dd');
const folderName = `${dateText}_${data.activityName}`;
const activityFolder = root.createFolder(folderName);
const subFolderNames = [
'01_校外教學計畫與公文',
'02_家長通知與同意書',
'03_回收資料與名冊',
'04_保險與交通',
'05_活動照片與紀錄',
'06_回饋與成果',
'07_經費與核銷',
'08_結案檢討與交接'
];
const subFolders = {};
subFolderNames.forEach(name => {
subFolders[name] = activityFolder.createFolder(name);
});
return { root: activityFolder, subFolders };
}
function createActivityDocs_(data, config, folders) {
const result = {};
if (config.TEMPLATE_PLAN_DOC_ID) {
const file = copyTemplateDoc_(
config.TEMPLATE_PLAN_DOC_ID,
`${data.activityName}_校外教學計畫`,
folders.subFolders['01_校外教學計畫與公文'],
data,
config
);
result.planUrl = file.getUrl();
}
if (config.TEMPLATE_NOTICE_DOC_ID) {
const file = copyTemplateDoc_(
config.TEMPLATE_NOTICE_DOC_ID,
`${data.activityName}_家長通知`,
folders.subFolders['02_家長通知與同意書'],
data,
config
);
result.noticeUrl = file.getUrl();
}
if (config.TEMPLATE_REPORT_DOC_ID) {
const file = copyTemplateDoc_(
config.TEMPLATE_REPORT_DOC_ID,
`${data.activityName}_成果報告`,
folders.subFolders['06_回饋與成果'],
data,
config
);
result.reportUrl = file.getUrl();
}
return result;
}
function copyTemplateDoc_(templateId, newName, targetFolder, data, config) {
const templateFile = DriveApp.getFileById(templateId);
const copiedFile = templateFile.makeCopy(newName, targetFolder);
const doc = DocumentApp.openById(copiedFile.getId());
const body = doc.getBody();
const replacements = {
'{{學校名稱}}': config.SCHOOL_NAME || '',
'{{活動名稱}}': data.activityName || '',
'{{活動類型}}': data.activityType || '',
'{{活動日期}}': formatDate_(data.activityDate, config),
'{{報名截止日}}': formatDate_(data.registrationDeadline, config),
'{{行前通知日}}': formatDate_(data.preNoticeDate, config),
'{{成果截止日}}': formatDate_(data.reportDeadline, config),
'{{承辦人}}': data.owner || '',
'{{協辦人}}': data.coOwner || '',
'{{參加對象}}': data.participants || '',
'{{預估人數}}': data.estimatedNumber || '',
'{{備註}}': data.note || ''
};
Object.keys(replacements).forEach(key => {
body.replaceText(key, String(replacements[key]));
});
doc.saveAndClose();
return copiedFile;
}
function createActivityCalendarEvents_(data, config, folderUrl) {
const calendar = CalendarApp.getCalendarById(config.CALENDAR_ID);
if (!calendar) throw new Error('找不到 Calendar,請檢查 CALENDAR_ID');
const result = {};
const description =
`活動名稱:${data.activityName}\n` +
`活動類型:${data.activityType}\n` +
`承辦人:${data.owner}\n` +
`參加對象:${data.participants}\n` +
`資料夾連結:${folderUrl}\n` +
`備註:${data.note || ''}`;
if (isValidDate_(data.registrationDeadline)) {
const event = calendar.createAllDayEvent(`【同意書回收】${data.activityName}`, new Date(data.registrationDeadline), { description });
result.registrationUrl = event.getId();
}
if (isValidDate_(data.preNoticeDate)) {
const event = calendar.createAllDayEvent(`【行前通知】${data.activityName}`, new Date(data.preNoticeDate), { description });
result.preNoticeUrl = event.getId();
}
if (isValidDate_(data.activityDate)) {
const event = calendar.createAllDayEvent(`【活動日】${data.activityName}`, new Date(data.activityDate), { description });
result.activityUrl = event.getId();
}
if (isValidDate_(data.reportDeadline)) {
const event = calendar.createAllDayEvent(`【成果截止】${data.activityName}`, new Date(data.reportDeadline), { description });
result.reportUrl = event.getId();
}
return result;
}
function updateActivityRow_(sheet, row, result, config) {
config = config || getConfig_();
if (result.activityId !== undefined) sheet.getRange(row, config.COL_ACTIVITY_ID).setValue(result.activityId);
if (result.status !== undefined) sheet.getRange(row, config.COL_STATUS).setValue(result.status);
if (result.folderUrl !== undefined) sheet.getRange(row, config.COL_FOLDER_URL).setValue(result.folderUrl);
if (result.planDocUrl !== undefined) sheet.getRange(row, config.COL_PLAN_DOC_URL).setValue(result.planDocUrl);
if (result.noticeDocUrl !== undefined) sheet.getRange(row, config.COL_NOTICE_DOC_URL).setValue(result.noticeDocUrl);
if (result.reportDocUrl !== undefined) sheet.getRange(row, config.COL_REPORT_DOC_URL).setValue(result.reportDocUrl);
if (result.registrationEventUrl !== undefined) sheet.getRange(row, config.COL_REG_EVENT_URL).setValue(result.registrationEventUrl);
if (result.preNoticeEventUrl !== undefined) sheet.getRange(row, config.COL_PRE_NOTICE_EVENT_URL).setValue(result.preNoticeEventUrl);
if (result.activityEventUrl !== undefined) sheet.getRange(row, config.COL_ACTIVITY_EVENT_URL).setValue(result.activityEventUrl);
if (result.reportEventUrl !== undefined) sheet.getRange(row, config.COL_REPORT_EVENT_URL).setValue(result.reportEventUrl);
if (result.calendarStatus !== undefined) sheet.getRange(row, config.COL_CALENDAR_STATUS).setValue(result.calendarStatus);
if (result.closeStatus !== undefined) sheet.getRange(row, config.COL_CLOSE_STATUS).setValue(result.closeStatus);
if (result.errorMessage !== undefined) sheet.getRange(row, config.COL_ERROR).setValue(result.errorMessage);
}
function formatDate_(value, config) {
if (!isValidDate_(value)) return '';
return Utilities.formatDate(new Date(value), config.TIMEZONE || 'Asia/Taipei', 'yyyy/MM/dd');
}
function isValidDate_(value) {
return value && Object.prototype.toString.call(new Date(value)) === '[object Date]' && !isNaN(new Date(value));
}
十一、第二堂課學習成果
| 類型 | 學習成果 |
|---|---|
| 概念成果 | 理解活動行政不是單一任務,而是多階段、多資料、多對象、多時間節點的工作流。 |
| 設計成果 | 能設計校外教學生命週期表、活動資料模型與活動資料夾模板。 |
| 技術成果 | 能使用活動安裝器產生 Apps Script,並讓活動建案表自動產生資料夾、文件範本、日曆提醒與總表狀態。 |
| 驗證成果 | 能提交活動資料夾連結、總表連結或截圖、日曆事件建立結果。 |
十二、校外教學工作流設計決策
本堂課以「五年級校外教學」作為活動型行政專案範例。系統的角色是建立一致的資料位置、提醒節點與追蹤紀錄;正式核定、名冊確認、保險送件、交通安排與核銷判斷仍由承辦人依校內流程確認。
| 設計決策 | 本堂課採用方式 | 行政判斷原則 |
|---|---|---|
| 目的地與交通資訊 | 活動建案表先保存目的地、集合時間、預估人數、交通需求與承辦人資訊;活動計畫與家長通知會放入可後續補正的文字欄位。 | 系統負責整理資訊與建立資料夾;正式車次、車號、司機資料與臨時異動仍由承辦人確認後補入。 |
| 同意書回收期限 | 本課以表單欄位「報名截止日」作為校外教學的同意書回收期限,並建立 Calendar 提醒。 | 本堂不做逐位學生追蹤,避免課程偏離活動建案與流程展開;個別追蹤可作為進階功能。 |
| 保險與交通資料 | 系統自動建立「04_保險與交通」資料夾,保留保險名冊、交通分車、場館聯繫與相關附件的位置。 | 正式保險名冊與交通資料不得只依系統產出判定,仍需承辦人依校內規定核對。 |
| 三份核心文件 | 活動安裝器自動建立校外教學計畫、家長通知、成果報告三份基礎範本。 | 三份文件分別對應規劃、通知、結案三個階段,你可以理解活動專案每個階段都應有交付成果。 |
| Calendar 事件 | 建立同意書回收、行前通知、活動日、成果截止四個節點。 | 活動不是只有活動日,而是一串會影響後續工作的期限;Calendar 用來讓期限顯性化。 |
| 報名系統界線 | 本堂課不自動建立完整報名表,也不處理報名上限、個別學生追蹤、家長自動通知與名冊驗證。 | 第二堂聚焦活動建案與流程展開;完整報名與名冊驗證可作為第三堂或第四堂延伸。 |
| 系統與人工分工 | Apps Script 建立表單、資料夾、文件、日曆與總控表紀錄。 | 系統整理資料,人負責最後確認;這是校外教學行政自動化的基本界線。 |