JS / 以Google表單為基礎結合日曆與試算表,用GAS建置簡易教室借用系統-表單觸發

這個程式碼是修改自以前的練習

但是讓Gemini用模組化的方式整理

表單架構如下

由於表單的日期、時間是2種類型,所以要分開來設計題目

第1步,在表單的功能選單,將表單連結至Google Apps Script

第2步,編輯GAS程式碼

總共有一個主程序+6個功能函數

比較需要說明的是#21 跟模組3

由於表單只有設定填寫借用者信箱,沒有設定其他資料,例如:姓名、電話等等

如果要再增加這樣就會影響到已經建立的表單試算表

所以在#98-101,分割"借用者信箱"的文字資料,取得@之前的字串作為借用者姓名

算是個折衷處理

如果表單有這些選項的話,就不需要這樣處理

但就會有其他判斷式,來判斷項目名稱(title),再來取得回應內容,如同取得"借用者信箱"的方式

 

模組6是用來確認通知信是否寄出

將資料寫入表單試算表的名稱為"信件內容"的工作表

 

整體程式碼如下

  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
/**
 * 主要函式:處理表單提交並寄送確認郵件。
 * @param {GoogleAppsScript.Events.FormsOnFormSubmit} e 觸發事件物件,包含表單回應資訊。
 */
function sendmail_44(e) {
  // 1. 鎖定機制:確保單一執行
  const lock = getScriptLock(15000);
  if (!lock) return; // 如果無法取得鎖,則退出

  try {
    // 2. 取得表單回應
    const formResponse = getLastFormResponse();
    if (!formResponse) {
      Logger.log("無法取得表單回應。");
      return;
    }

    const itemResponses = formResponse.getItemResponses();

    // 3. 建立郵件內容
    const { htmlBody, userName ,userEmail} = createEmailContent(itemResponses);

    // 4. 建立 PDF 附件
    let pdfFile = null;
    try {
      pdfFile = createPdfAttachment(htmlBody, userName);
    } catch (pdfError) {
      Logger.log("建立 PDF 發生錯誤: %s", pdfError.toString());
      // 即使 PDF 失敗,仍嘗試寄送 Email
    }

    // 5. 寄送郵件

    sendConfirmationEmail(userEmail, htmlBody, pdfFile);

    // 6. 記錄到試算表
    const spreadsheetId = "*************************************"; // 請替換為您的實際 ID //https://docs.google.com/spreadsheets/d/*************************************/
    const sheetName = "信件內容";
    logToSpreadsheet(spreadsheetId, sheetName, userEmail, userName, htmlBody);

  } catch (error) {
    Logger.log("主要執行發生錯誤: %s", error.toString());
  } finally {
    // 釋放鎖定
    lock.releaseLock();
  }
}

/**
 * 模組 1: 處理腳本鎖定。
 * @param {number} timeoutMillis 鎖定等待時間 (毫秒)。
 * @returns {GoogleAppsScript.Lock.Lock | null} 腳本鎖物件或 null (如果失敗)。
 */
function getScriptLock(timeoutMillis) {
  const lock = LockService.getScriptLock();
  try {
    lock.waitLock(timeoutMillis);
    return lock;
  } catch (e) {
    Logger.log("無法取得鎖定: %s", e.toString());
    return null;
  }
}

/**
 * 模組 2: 取得表單的最後一筆回應。
 * @returns {GoogleAppsScript.Forms.FormResponse | null} 最後一個 FormResponse 物件。
 */
function getLastFormResponse() {
  const form = FormApp.getActiveForm();
  const formResponses = form.getResponses();
  const lastIndex = formResponses.length - 1;

  if (lastIndex < 0) {
    return null;
  }
  return formResponses[lastIndex];
}

/**
 * 模組 3: 根據表單回應建立 HTML 郵件內容。
 * @param {GoogleAppsScript.Forms.ItemResponse[]} itemResponses 表單問題和回應的陣列。
 * @returns {{htmlBody: string, userName: string, userEmail: string}} 包含 HTML 內容和使用者姓名、信箱的物件。
 */
function createEmailContent(itemResponses) {
  let userName = "借用者";
  let userEmail="";
  let answerHtml = '<p><strong>請確認您所填寫的借用資訊</strong></p><p><strong>如果需要修改,請聯絡本中心承辦人</strong></p>';
  answerHtml += '<div class="wrapper" style="padding: 20px;max-width: 900px;">';
  answerHtml += '<div class="table" style="width: 100%;box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);display: table;">';
  answerHtml += '<div class="row header blue" style="display: table-row;background: #2980b9;font-weight: 900;color: #ffffff;"><div class="cell" style="padding: 6px 12px;display: table-cell;width: 35%">項目</div><div class="cell" style="padding: 6px 12px;display: table-cell;">回答內容</div></div>';

  for (const itemResponse of itemResponses) {
    const title = itemResponse.getItem().getTitle().trim();
    Logger.log(title);
    const response = itemResponse.getResponse();

    if (title == "借用者信箱") {
      userEmail = response;
      userName = response.split("@")[0];
    }

    // 串接內容
    answerHtml += '<div class="row" style="display: table-row;"><div class="cell" style="padding: 6px 12px;display: table-cell;background: #f6f6f6;">' + title + '</div><div class="cell" style="padding: 6px 12px;display: table-cell;">' + response + '</div></div>';
  }

  answerHtml += '</div></div>';
  answerHtml += '<p>--<br>XXXXXXXXXXXXX中心<br />聯絡人:XXX助理<br />電話:(02)1111-2222<br />地址:106 臺北市大安區XXXXXX</p>';

  const htmlBody = HtmlService.createHtmlOutput(answerHtml).getContent();
  return { htmlBody, userName ,userEmail};
}


/**
 * 模組 4: 將 HTML 內容轉換為 PDF 附件,並暫時儲存在 Drive。
 * @param {string} htmlText 包含報名資訊的 HTML 內容。
 * @param {string} userName 使用者姓名,用於 PDF 檔名。
 * @returns {GoogleAppsScript.Drive.File} 建立的 PDF 檔案物件。
 */
function createPdfAttachment(htmlText, userName) {
  const blob = Utilities.newBlob(htmlText, "text/html", "temp.html");
  const pdfBlob = blob.getAs("application/pdf");
  const pdfFile = DriveApp.createFile(pdfBlob).setName(userName + "-借用資料.pdf");
  Logger.log("PDF 檔案已建立: %s", pdfFile.getDownloadUrl());
  return pdfFile;
}

/**
 * 模組 5: 寄送確認電子郵件。
 * @param {string | undefined} emailTo 收件人電子郵件地址。
 * @param {string} htmlBody 郵件的 HTML 內容。
 * @param {GoogleAppsScript.Drive.File | null} pdfFile PDF 附件檔案物件 (如果存在)。
 */
function sendConfirmationEmail(emailTo, htmlBody, pdfFile) {
  if (!emailTo) {
    Logger.log("收件人電子郵件地址無效,跳過寄信。");
    // 即使沒有 Email,也執行 PDF 檔案清理
    if (pdfFile) {
      DriveApp.getFileById(pdfFile.getId()).setTrashed(true);
    }
    return;
  }

  const subject = "場地借用-申請資料";
  const options = {
    htmlBody: htmlBody,
    name: "XXXXXX-系統自動回覆信件",
    replyTo: "abc@ntnu.edu.tw",
    cc: "abc@ntnu.edu.tw",
    attachments: pdfFile ? [pdfFile.getBlob()] : [] // 如果有 PDF 才加入附件
  };

  try {
    GmailApp.sendEmail(emailTo, subject, "", options);
    Logger.log("郵件已成功寄給: %s", emailTo);
  } catch (e) {
    Logger.log("寄送郵件發生錯誤: %s", e.toString());
  } finally {
    // 寄出信件後,刪除暫存的 PDF
    if (pdfFile) {
      DriveApp.getFileById(pdfFile.getId()).setTrashed(true);
      Logger.log("PDF 檔案已刪除。");
    }
  }
}

/**
 * 模組 6: 將寄件資訊寫入試算表。
 * @param {string} ssId 試算表的 ID。
 * @param {string} sheetName 工作表的名稱。
 * @param {string | undefined} emailTo 收件人電子郵件地址。
 * @param {string} userName 使用者姓名。
 * @param {string} answerHtml 郵件的 HTML 內容。
 */
function logToSpreadsheet(ssId, sheetName, emailTo, userName, answerHtml) {
  try {
    const ss = SpreadsheetApp.openById(ssId);
    const sOne = ss.getSheetByName(sheetName);
    if (!sOne) {
      Logger.log("找不到工作表: %s", sheetName);
      return;
    }

    const dataRow = sOne.getLastRow();
    const dataColumn = sOne.getLastColumn() || 3; // 至少確保有 4 欄
    
    const dataVlues = [[emailTo || "N/A", userName, answerHtml]];
    
    // 寫入下一行
    sOne.getRange(dataRow + 1, 1, 1, dataVlues[0].length).setValues(dataVlues);
    Logger.log("資料已寫入試算表。");
  } catch (e) {
    Logger.log("寫入試算表發生錯誤: %s", e.toString());
  }
}

 

AI重新整理過的程式碼,相較之下

有很完整的程式備註與錯誤處理程序

 

第3步,建立觸發行為

當表單有新回應的時候,觸發程式

 

這樣借用人送出表單之後,就會收到通知信跟附件