《零基礎快速入門:GenAI 搭配 Google Apps Script 的工作自動化寶典》Chapter 5-3 輕鬆打造客製化預約系統

引言

今天要帶來一個蠻進階的案例:服務預約系統。
之所以說進階,
是因為這個案例將會同時使用到前端、後端,
而且讓前後端是可以互相溝通的!

做完這個案例,
你也許就可以自稱自己是一個全端工程師了。
只是年資經驗很淺而已😆

但這是個起點,
當你搞懂這一篇的基本範例,
接下來就可以自行發揮創意了!

需求背景

有些公司裡有常駐視障按摩師,既能符合進用障礙人士的勞動法規,
又能順便作為員工福利的一環。

為了有效地安排每個人的按摩時段,我們就需要做一個客製化的預約系統。
其實在最簡單的情況下我們可以用 Google 行事曆來當預約系統就可以了。
誰要預約哪個時段,就發一個會議邀請過去即可。

但一般的行事曆有幾個缺點,
第一他沒有辦法限定每個人的預約時間長度。
造成有人預約15分鐘,結果有人預約長達1小時。

第二就是一般行事曆沒辦法限制每個人只能預約一次。
怕有人濫用佔用大家的福利。

你可以這樣向 AI 提問

基本上也是遵循「我有什麼食材,我想用什麼手法,我想做出什麼料理」的提問概念:

我在 Google Sheets 上有一個日曆表格如下:
Time Slot Monday Tuesday Wednesday Thursday Friday
09:00 – 09:15
09:15 – 09:30
09:30 – 09:45
09:45 – 10:00
10:00 – 10:15
10:15 – 10:30
10:30 – 10:45
10:45 – 11:00
11:00 – 11:15
11:15 – 11:30
11:30 – 11:45
11:45 – 12:00
12:00 – 12:15
12:15 – 12:30
12:30 – 12:45
12:45 – 13:00
13:00 – 13:15
13:15 – 13:30
13:30 – 13:45
13:45 – 14:00
14:00 – 14:15
14:15 – 14:30
14:30 – 14:45
14:45 – 15:00
15:00 – 15:15
15:15 – 15:30
15:30 – 15:45
15:45 – 16:00
16:00 – 16:15
16:15 – 16:30
16:30 – 16:45
16:45 – 17:00
17:00 – 17:15
17:15 – 17:30
17:30 – 17:45
17:45 – 18:00
我想要基於這個表格,在網頁上顯示出一樣的表格,
並且在每個空格裡提供一個「預約」的按鍵,
讓使用者在網頁上按下去之後,可以填入姓名,並且自動回填至 Google Sheets 上。
當某一個已經有人預約之後,
網頁上的該格子就要讓「預約」按紐 gary out,不讓使用者再按。

最終程式碼

跟 ChatGPT 來回的微調討論過程我就不贅述了,
歡迎直接看完整對話紀錄:https://chatgpt.com/share/6703ebfc-3448-800e-969c-1efe51b1a7ce

在此附上最終程式碼

code.gs

function doGet() {
    return HtmlService.createHtmlOutputFromFile('index');
  }

  function getCalendarData() {
    const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('calendar'); // 假設你的表格名稱是 'Sheet1'
    const dataRange = sheet.getDataRange();
    const data = dataRange.getValues();
    return data;
  }

  function bookSlot(row, col, name) {
    const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('calendar');
    const currentValue = sheet.getRange(row, col).getValue();
    if (currentValue === "") {
      sheet.getRange(row, col).setValue(name);
      return true; // 預約成功
    } else {
      return false; // 已被預約
    }
  }

index.html

  <!DOCTYPE html>
  <html>
    <head>
      <base target="_top">
      <style>
        body {
          font-family: Arial, sans-serif;
          background-color: #f9f9f9;
          color: #333;
          text-align: center;
          padding: 20px;
        }
        table {
          width: 100%;
          max-width: 800px;
          margin: 0 auto;
          border-collapse: collapse;
        }
        table, th, td {
          border: 1px solid #ddd;
        }
        th, td {
          padding: 10px;
          text-align: center;
        }
        th {
          background-color: #4CAF50;
          color: white;
        }
        td {
          background-color: #fff;
        }
        td.booked {
          background-color: #f0f0f0;
          color: #777;
        }
        button {
          padding: 8px 16px;
          background-color: #4CAF50;
          color: white;
          border: none;
          cursor: pointer;
          border-radius: 5px;
          transition: background-color 0.3s;
        }
        button:hover {
          background-color: #45a049;
        }
        button:disabled {
          background-color: #ccc;
          cursor: not-allowed;
        }
      </style>
    </head>
    <body>
      <h1>預約時段日曆</h1>
      <div id="calendar"></div>

      <script>
        function loadCalendar() {
          google.script.run.withSuccessHandler(renderCalendar).getCalendarData();
        }

        function renderCalendar(data) {
          const calendarDiv = document.getElementById('calendar');
          calendarDiv.innerHTML = ''; // 清空舊的內容

          const table = document.createElement('table');

          data.forEach((row, rowIndex) => {
            const tr = document.createElement('tr');

            row.forEach((cell, colIndex) => {
              const td = document.createElement('td');
              if (rowIndex === 0 || colIndex === 0) {
                // 表頭
                td.textContent = cell;
                td.style.fontWeight = 'bold';
              } else {
                if (cell === "") {
                  const button = document.createElement('button');
                  button.textContent = '預約';
                  button.onclick = function() {
                    const name = prompt("請輸入您的姓名:");
                    if (name) {
                      google.script.run.withSuccessHandler((success) => {
                        if (success) {
                          alert("預約成功!");
                          loadCalendar(); // 重新載入日曆
                        } else {
                          alert("該時段已被預約,請選擇其他時段。");
                        }
                      }).bookSlot(rowIndex + 1, colIndex + 1, name);
                    }
                  };
                  td.appendChild(button);
                } else {
                  td.textContent = cell + ' (已預約)';
                  td.classList.add('booked');
                }
              }
              tr.appendChild(td);
            });

            table.appendChild(tr);
          });

          calendarDiv.appendChild(table);
        }

        loadCalendar();
      </script>
    </body>
  </html>

後端 Google Sheets 的長相

做出來的前端頁面成果


可以點這個連結看真實效果:
https://script.google.com/macros/s/AKfycbw2p6HMsSaL-B3zPdHQcPu1yK7F6ZMhdm-ZuSLMWKNSh2ScXuNJHFXEf0SZyQ9IEJRzFw/exec

還可以再加哪些功能

這個範例很基本,
所以後續還可以做的改善還很多。
例如可以加上:

  • 讓使用者使用Google帳號登入再進行預約
  • 讓使用者只能預約一次
  • 讓使用者可以取消他先前的預約
  • 在預約時間快到的前半小時自動發信提醒使用者

結語

其實一般正統的程式設計師是不會這樣子做的。
一般的預約系統的所有資料,包含姓名、時間等等,
通常會存在一個非常結構化的資料庫表格裡面。

但是正因為我們不是專業的工程師我們才能跳脫思考的框框,
改成用 Google Sheets 的表格直接來當作儲存的載體。

直接以日曆視覺化的方式儲存資料,
而且也可以把後端儲存的東西直接原樣照搬到前端上。

是一個蠻有意思的解決方案!