接下來進入到審核階段
工作表的架構
A-G欄是表單資料產生的
H欄是自訂用來記錄跟程式判斷是否進行寫入日曆的依據
I-J欄是程式寫出的資訊

第1步,透過表單回覆資料的工作表,連結到GAS

第2步,編輯GAS程式碼
總共有1個主程式+4個功能函數+1個內建的函數onOpen()
onOpen()函數顧名思義,當試算表打開時會觸發
這裡用來執行模組4-更新資料驗證、在工作表增加自訂選單
模組4的程式碼是我額外加的
這裡要補充說明在前面提到的:AI搞錯的地方
假設要取得B2的資料,流程會是
1.先取得整個工作表的資料,會是個陣列
2.取得陣列的第2筆資料,陣列序號是從0開始,所以是陣列[1]
3.再取得第2筆資料的第2筆資料,所以是陣列[1][1]
AI在一開始的參數設定,就是用陣列序號的方式
例如:CLASSROOM_NAME: 1, // B 欄 借用教室名稱
所以在後面要抓取資料的時候
const dataRange = sheet.getDataRange(); //取得工作表資料範圍
const allData = dataRange.getValues(); //取得範圍內的資料
const row = allData[i]; //取得指定的列數所有的資料
再來就是取得特定欄位的資料,假如要取得的是B欄,程式碼應該是row[1],也就是下面的
const classroomName = row[CONFIG.COL_INDEX.CLASSROOM_NAME];
但是AI的程式碼一開始是
const classroomName = row[CONFIG.COL_INDEX.CLASSROOM_NAME-1];
所以會對應不到正確欄位
到了後面,要寫入試算表資料時,會用到getRange()函數
裡面的參數,序號是從1開始
所以假如要在J欄寫入資料,應該是寫入第10欄,CONFIG.COL_INDEX.MESSAGE是9,所以要+1
sheet.getRange(rowNumber, CONFIG.COL_INDEX.MESSAGE + 1).setValue('要寫入的內容');
這裡AI卻是
sheet.getRange(rowNumber, CONFIG.COL_INDEX.MESSAGE ).setValue('要寫入的內容');
這樣會寫到前一個欄位
模組4的程式碼也是問Gemini,給的程式碼整體架構是正確的
但是其中一個設定"拒絕輸入清單以外的值"的函數方法 ,AI是用"setAllowInvalidInput()"
程式碼跑到這裡就報錯,經過查詢官方文件,應該是 "setAllowInvalid()"
整體程式碼如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 |
// ==================================================================== // 全域設定 (請務必調整) // ==================================================================== const CONFIG = { SHEET_NAME: "表單回應 1", // 表單回應工作表名稱 // 請根據試算表欄位順序 (從 1 開始) 決定索引 (陣列從 0 開始) COL_INDEX: { CLASSROOM_NAME: 1, // 假設在 B 欄 // 由於表單將日期與時間拆分,請確認這四個欄位的索引 START_DATE: 2, // 假設在 C 欄 (開始日期) START_TIME: 3, // 假設在 D 欄 (開始時間) END_DATE: 4, // 假設在 E 欄 (結束日期) END_TIME: 5, // 假設在 F 欄 (結束時間) USER_EMAIL: 6, // 假設在 G 欄 STATUS: 7, // 假設在 H 欄 (手動更改為 Approved) EVENT_ID: 8, // 假設在 I 欄 (腳本寫入) MESSAGE: 9 // 假設在 J 欄 (腳本寫入) } }; // 教室名稱與對應 Google 日曆 ID 的映射 (請務必替換) const CALENDAR_MAP = { 'Room A - 101': '****************************************************************@group.calendar.google.com', 'Room B - 203': '****************************************************************@group.calendar.google.com', // 如果有更多教室,請在此處添加 }; // ==================================================================== // 步驟 1: 建立自訂選單 // ==================================================================== /** * 試算表開啟時自動執行,建立一個自訂選單按鈕、更新/建立 資料驗證 //模組4。 */ function onOpen() { //更新/建立 資料驗證 //模組4 createDropdownValidation(); //建立選單 const ui = SpreadsheetApp.getUi(); ui.createMenu('🛠 教室借用審核') .addItem('✅ 更新行事曆 (處理已批准請求)', 'processApprovedBookings') .addToUi(); Logger.log('自訂選單已建立。'); } // ==================================================================== // 步驟 2: 處理已批准請求的主邏輯 // ==================================================================== /** * 掃描試算表,處理所有狀態為 'Approved' 且尚未寫入日曆的請求。 * 由管理員點擊自訂選單按鈕觸發。 */ function processApprovedBookings() { const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(CONFIG.SHEET_NAME); if (!sheet) { Browser.msgBox('錯誤', `找不到工作表:${CONFIG.SHEET_NAME}。請檢查設定。`, Browser.Buttons.OK); return; } const dataRange = sheet.getDataRange(); const allData = dataRange.getValues(); let processedCount = 0; // 從第二列開始處理 (跳過標頭) for (let i = 1; i < allData.length; i++) { const row = allData[i]; const rowNumber = i + 1; // 實際的列號 // 陣列索引 const status = row[CONFIG.COL_INDEX.STATUS]; const eventId = row[CONFIG.COL_INDEX.EVENT_ID]; // ****** 除錯輸出:查看腳本讀到的值 ****** //Logger.log(`--- Row ${rowNumber} ---`); //Logger.log(row[7]) //Logger.log(row[CONFIG.COL_INDEX.STATUS]) // 檢查狀態是否為 'Approved' 且 'Calendar Event ID' 欄位為空 if (status === 'Approved' && (!eventId || eventId === '')) { const classroomName = row[CONFIG.COL_INDEX.CLASSROOM_NAME]; const userEmail = row[CONFIG.COL_INDEX.USER_EMAIL]; // 取得日期和時間的原始值 const startDate = row[CONFIG.COL_INDEX.START_DATE]; const startTimeVal = row[CONFIG.COL_INDEX.START_TIME]; const endDate = row[CONFIG.COL_INDEX.END_DATE]; const endTimeVal = row[CONFIG.COL_INDEX.END_TIME]; // 合併為完整的 Date 物件 // 模組1 const combinedStartTime = combineDateAndTime(startDate, startTimeVal); const combinedEndTime = combineDateAndTime(endDate, endTimeVal); if (!combinedStartTime || !combinedEndTime) { // 合併失敗,標記錯誤 sheet.getRange(rowNumber, CONFIG.COL_INDEX.STATUS + 1).setValue('Error'); sheet.getRange(rowNumber, CONFIG.COL_INDEX.MESSAGE + 1).setValue('日期或時間格式錯誤,無法解析。'); sendNotificationEmail(userEmail, title, null, 'error', '日期時間格式錯誤,請聯繫管理員。'); continue; } // 執行檢查並寫入行事曆的邏輯 // 模組2 const title = `已核准借用: ${classroomName}`; const result = createCalendarEvent(classroomName, combinedStartTime, combinedEndTime, title, userEmail); // 更新試算表 if (result.success) { // 成功寫入 sheet.getRange(rowNumber, CONFIG.COL_INDEX.STATUS + 1).setValue('Completed'); sheet.getRange(rowNumber, CONFIG.COL_INDEX.EVENT_ID + 1).setValue(result.id); sheet.getRange(rowNumber, CONFIG.COL_INDEX.MESSAGE + 1).setValue('Success'); sendNotificationEmail(userEmail, title, result.id, 'success'); processedCount++; } else { // 寫入失敗 (因邏輯錯誤或日曆衝突) sheet.getRange(rowNumber, CONFIG.COL_INDEX.STATUS + 1).setValue('Error'); sheet.getRange(rowNumber, CONFIG.COL_INDEX.MESSAGE + 1).setValue(`處理失敗: ${result.message}`); sendNotificationEmail(userEmail, title, null, 'error', result.message); // 模組3 processedCount++; } } } // 執行完成後給管理員的提示 Browser.msgBox('日曆更新完成', `總共處理了 ${processedCount} 個已批准的請求。請查看試算表中的狀態欄位。`, Browser.Buttons.OK); } // ==================================================================== // 模組1: 日期時間合併輔助函式 // ==================================================================== /** * 將 Google Sheet 中的日期物件和時間值合併成一個完整的 Date 物件。 */ function combineDateAndTime(datePart, timePart) { try { const date = new Date(datePart); if (isNaN(date.getTime())) return null; // 無效日期 let timeStr = ''; if (typeof timePart === 'string') { timeStr = timePart; } else if (timePart instanceof Date) { // 如果是 Date 物件 (通常來自 Sheet 的時間欄位),格式化為 HH:mm:ss const timeZone = SpreadsheetApp.getActive().getSpreadsheetTimeZone(); timeStr = Utilities.formatDate(timePart, timeZone, 'HH:mm:ss'); } else { return null; } const year = date.getFullYear(); const month = date.getMonth(); const day = date.getDate(); const timeParts = timeStr.split(':'); const hours = parseInt(timeParts[0]); const minutes = parseInt(timeParts[1]); const seconds = parseInt(timeParts[2] || 0); // 組合新的 Date 物件 (使用本地時間設定) return new Date(year, month, day, hours, minutes, seconds); } catch (e) { Logger.log('合併日期時間時發生錯誤: ' + e); return null; } } // ==================================================================== // 模組2: 檢查衝突與寫入日曆 // ==================================================================== /** * 執行所有邏輯檢查 (時間順序、跨天、日曆衝突) 並寫入日曆。 */ function createCalendarEvent(classroomName, startTime, endTime, title, userEmail) { const calendarId = CALENDAR_MAP[classroomName]; if (!calendarId) return { success: false, message: '錯誤:無效的教室名稱或日曆ID未設定。' }; // 1. 【檢查點:時間邏輯】開始時間不能晚於或等於結束時間 if (startTime.getTime() >= endTime.getTime()) { return { success: false, message: '邏輯錯誤:開始時間晚於或等於結束時間。' }; } // 2. 【檢查點:跨天邏輯】不能跨天 (確保年/月/日相同) const timeZone = SpreadsheetApp.getActive().getSpreadsheetTimeZone(); const startDateStr = Utilities.formatDate(startTime, timeZone, 'yyyy-MM-dd'); const endDateStr = Utilities.formatDate(endTime, timeZone, 'yyyy-MM-dd'); if (startDateStr !== endDateStr) { return { success: false, message: '邏輯錯誤:借用時間不能跨越日期。' }; } // 3. 【檢查點:日曆衝突】 const calendar = CalendarApp.getCalendarById(calendarId); try { const conflictingEvents = calendar.getEvents(startTime, endTime); if (conflictingEvents.length > 0) { const conflictTitle = conflictingEvents[0].getTitle(); return { success: false, message: `日曆衝突:該時段已被預訂 (${conflictTitle})。` }; } } catch (e) { return { success: false, message: `日曆讀取失敗:請檢查日曆ID和權限。錯誤: ${e}` }; } // 4. 【通過所有檢查,執行寫入】 try { const newEvent = calendar.createEvent( title, startTime, endTime, { description: `教室借用系統核准\n借用人信箱: ${userEmail}`, sendInvites: false, guests: userEmail } ); return { success: true, id: newEvent.getId() }; } catch (e) { return { success: false, message: `日曆寫入失敗:發生未預期的錯誤。錯誤: ${e}` }; } } // ==================================================================== // 模組3: 通知郵件函式 // ==================================================================== /** * 發送電子郵件通知借用者審核結果。 */ function sendNotificationEmail(recipient, title, eventId, status, errorMessage) { let subject; let body; if (status === 'success') { subject = `✅ 借用申請已核准:${title}`; body = `親愛的借用者:\n\n您的教室借用申請已由管理員核准並登錄至行事曆。\n\n` + `活動標題:${title}\n` + `活動 ID:${eventId}\n\n` + `請查看您的 Google 日曆以確認活動細節。\n\n` + `此為系統自動發送,請勿直接回覆。`; } else if (status === 'error') { subject = `❌ 借用申請處理失敗:${title}`; body = `親愛的借用者:\n\n您的教室借用申請日期時間發生錯誤,無法完成預訂。\n\n` + `失敗原因:${errorMessage || '未知的系統錯誤。'}\n` + `請聯繫管理員處理。`; } if (subject && body) { try { MailApp.sendEmail(recipient, subject, body); Logger.log(`成功發送通知信給 ${recipient}`); } catch (e) { Logger.log(`發送通知信失敗給 ${recipient}: ${e}`); } } } // ==================================================================== // 模組4: 更新/建立 資料驗證 // ==================================================================== function createDropdownValidation() { const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(CONFIG.SHEET_NAME); if (!sheet) { Browser.msgBox('錯誤', `找不到工作表:${CONFIG.SHEET_NAME}。請檢查設定。`, Browser.Buttons.OK); return; } const dataRange = sheet.getDataRange(); const allData = dataRange.getValues(); const lastRow = allData.length; Logger.log(lastRow); // 1. 定義下拉式選單的選項 var items = ['Approved', 'Completed', 'Pending', 'Cancel']; // 2. 建立資料驗證規則 var rule = SpreadsheetApp.newDataValidation() .requireValueInList(items) // 要求值必須在定義的清單中 .setAllowInvalid(false) // 設定為拒絕輸入清單以外的值 .setHelpText("請從清單中選擇一個有效的選項。") // 設定說明文字 .build(); // 3. 將規則應用到指定的範圍(例如 A2:A10) sheet.getRange(2, 8, lastRow - 1).setDataValidation(rule); Logger.log("資料驗證規則已設定完成。"); } |
第3步,手動執行審核
如果資料無誤,可以借用,在相對應的H欄,選擇"Approved"
然後從選單中執行程序

這樣應該就會發出審核通過信件了
![]()
行事曆上也會有這個活動
