Google sheet Apps script | Todo List and Send mail – Tạo danh sách nhắc việc và gửi mail

Trong bài này, giaoan.link chia sẻ đến các bạn một project khá thú vị về “Google sheet Apps script | Todo List and Send mail – Tạo danh sách nhắc việc và gửi mail”.

Trong project webapp này có các chức năng như sau:

  • Cập nhật, lên lịch sự kiện từ form nhập liệu giao diện web.
  • Load danh sách sự kiện có kèm hình ảnh lên giao diện web để quản lý.
  • Có chức năng xóa một sự kiện khi không cần, hoặc đã thực hiện xong.
  • Khi đến thời hạn ngày thực hiện công việc, tự động mail gửi nhác việc đến địa chỉ email deploy webapp.

Dưới đây là video hướng dẫn và demo cùng code mã apps script

Bạn tìm thêm nhiều project googleapps script tại đây.

Các project hiển thị ngẫu nhiên:

Mã apps script “Code.gs”

/*https://youtube.com/@netmediacctv
  website: https://giaoan.link
*/
function doGet() {
  return HtmlService.createHtmlOutputFromFile('Index');
}

function addTodoItem(title, description, dueDate, image) {
  const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('TodoList');
  const lastRow = sheet.getLastRow();
  const nextRow = lastRow + 1;

  // Tạo thư mục "images" nếu chưa tồn tại
  let folder = DriveApp.getFoldersByName("images").hasNext() ? DriveApp.getFoldersByName("images").next() : DriveApp.createFolder("images");

  // Upload file
  const blob = Utilities.newBlob(Utilities.base64Decode(image.bytes), image.mimeType, image.filename);     
  const file = folder.createFile(blob);      
  const imageUrl = file.getUrl();

  sheet.getRange(nextRow, 1).setValue(title);
  sheet.getRange(nextRow, 2).setValue(description);
  sheet.getRange(nextRow, 3).setValue(new Date(dueDate));
  sheet.getRange(nextRow, 4).setValue(imageUrl);

  // Kiểm tra nếu dueDate trùng với ngày hiện tại
  const today = new Date().toDateString();
  if (new Date(dueDate).toDateString() === today) {
    sendEmailReminder(title, description);
  } else {
    // Lên lịch gửi email vào 1 giờ sáng
    const triggerDate = new Date(dueDate);
    triggerDate.setHours(1, 0, 0, 0);
    ScriptApp.newTrigger('sendEmailReminderTrigger')
      .timeBased()
      .at(triggerDate)
      .create();
  }

  // Trả về danh sách mới sau khi thêm
  return getTodoItems();
}

function sendEmailReminderTrigger() {
  checkDueDates();
}

function getTodoItems() {
  const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('TodoList');
  if (!sheet) {
    return [];
  }

  const data = sheet.getDataRange().getValues();
  if (!data || data.length === 0) {
    return [];
  }

  return data.slice(1).map((row, index) => ({
    id: index + 1, // Thêm ID để nhận diện
    title: row[0] || '',
    description: row[1] || '',
    dueDate: row[2] ? new Date(row[2]).toISOString() : '',
    imageUrl: row[5] || ''
  }));
}

function deleteTodoItem(itemId) {
  const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('TodoList');
  const data = sheet.getDataRange().getValues();

  if (itemId > 0 && itemId < data.length) {
    sheet.deleteRow(itemId + 1); // Xóa dòng tương ứng trong sheet
  }
}

function checkDueDates() {
  const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('TodoList');
  if (!sheet) {
    return;
  }

  const data = sheet.getDataRange().getValues();
  if (!data || data.length === 0) {
    return;
  }

  const today = new Date().toDateString();
  data.slice(1).forEach(row => {
    const dueDate = new Date(row[2]).toDateString();
    if (dueDate === today) {
      sendEmailReminder(row[0], row[1]);
    }
  });
}

function sendEmailReminder(title, description) {
  const emailAddress = Session.getActiveUser().getEmail();
  const subject = `Lời nhắc nhở: ${title}`;
  const body = `Đừng quên sự kiện: ${title}\n\nMô tả sự kiện: ${description}`;

  MailApp.sendEmail(emailAddress, subject, body);
}

function createDailyTrigger() {
  ScriptApp.newTrigger('sendEmailReminderTrigger')
    .timeBased()
    .everyDays(1)
    .atHour(8)
    .create();
}

Mã apps script “Index.html”

<!DOCTYPE html>
<html>
<head>
  <base target="_top">
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.9.1/font/bootstrap-icons.css">
  <style>
    body{
      font-family: Poppins, sans-serif;
      border: 1px solid #fab000;
      border-radius: 8px;
      box-shadow: 2px 2px 3px grey;
      width: 95%;      
      padding: 10px;
      margin-left: auto;
      margin-right: auto;
    }

    .div-block{
      display: block;
      gap: 20px;
    }
    .div-form{
      border: 1px solid grey;
      border-radius: 5px;      
      width: 600px;
      padding: 5px;
      margin-left: auto;
      margin-right: auto;
      margin-bottom: 20px;
    }

    input{
      font-size: 0.9em;
      width: 97%;
      margin-top: 3px;
      margin-bottom: 10px;
      border: 1px solid #fab000;
      border-radius: 5px;
      box-shadow: 1px 1px 1px grey;
      padding: 8px;
    }

    .title-textarea{
      font-size: 1.2em;
      width: 97%;
      margin-top: 3px;
      margin-bottom: 10px;
      border: 1px solid #fab000;
      border-radius: 5px;
      box-shadow: 1px 1px 1px grey;
      padding: 8px;
    }

    .title{
      font-size: 1.5em;
      font-weight: bold;
      text-align: center;
      color: blue;
    }

    .title1{
      font-size: 1em;
      font-weight: bold;
      font-style: italic;
      text-align: center;
      color:red;
      margin-bottom: 20px;
    }

    label{
      font-size: 1em;
      font-weight: bold;
    }
  
    table {
      width: 100%;
      border-collapse: collapse;
    }
    th, td {
      border: 1px solid #ccc;     
      padding: 8px;      
    }
    th {
      text-align: center;
      background-color: #ffcd48;
    }
    button.delete-button {
      background-color: #ff6666;
      color: white;
      border: none;
      padding: 5px 10px;
      cursor: pointer;
    }
    .bnt{      
      font-size: 1em;
      font-weight: bold;
      margin-top: 15px;
      padding: 10px 15px;
      border: 1px solid grey;
      background-color: #fab000;
      border-radius: 8px;
      outline: none;
      cursor: pointer;
    }
    .bnt:hover{
      box-shadow: 2px 2px 3px grey;
    }
    @media (max-width: 800px){
      .div-form{      
        width: 95%;      
      }
      input{
      width: 95%;
    }

    .title-textarea{
      width: 95%;
    }
  </style>
  <script>
    function addTodoItem() {
      const title = document.getElementById('title').value;
      const description = document.getElementById('description').value;
      const dueDate = document.getElementById('dueDate').value;
      const image = document.getElementById('image').files[0];
      const reader = new FileReader();

      reader.onload = function(e) {
        const imageData = {
          bytes: e.target.result.split(',')[1],
          mimeType: image.type,
          filename: image.name
        };
        google.script.run.withSuccessHandler(displayTodoItems).addTodoItem(title, description, dueDate, imageData);
        document.getElementById('todoForm').reset();
      };

      reader.readAsDataURL(image);
    }

    function loadTodoItems() {
      google.script.run.withSuccessHandler(displayTodoItems).getTodoItems();
    }

    function deleteTodoItem(itemId) {
      google.script.run.withSuccessHandler(loadTodoItems).deleteTodoItem(itemId);
    }

    function displayTodoItems(items) {
      console.log('Todo items:', items); // Log ra console để kiểm tra

      const list = document.getElementById('todoList');
      list.innerHTML = '';

      if (!items || items.length === 0) {
        list.innerHTML = '<p>Không tìm thấy danh sách việc làm.</p>';
        return;
      }

      const table = document.createElement('table');
      const headerRow = document.createElement('tr');
      ['Tiêu đề', 'Mô tả công việc', 'Ngày thực hiện', 'Hình ảnh', 'Hành động'].forEach(text => {
        const th = document.createElement('th');
        th.textContent = text;
        headerRow.appendChild(th);
      });
      table.appendChild(headerRow);

      items.forEach(item => {
        const row = document.createElement('tr');

        const titleCell = document.createElement('td');
        titleCell.textContent = item.title;
        row.appendChild(titleCell);

        const descriptionCell = document.createElement('td');
        descriptionCell.textContent = item.description;
        row.appendChild(descriptionCell);

        const dueDateCell = document.createElement('td');
        dueDateCell.style = 'text-align: center';
        dueDateCell.textContent = item.dueDate ? new Date(item.dueDate).toLocaleDateString() : 'No due date';
        row.appendChild(dueDateCell);

        const imageCell = document.createElement('td');
        imageCell.style = 'text-align: center';
        if (item.imageUrl) {
          const image = document.createElement('img');
          image.src = item.imageUrl;
          
          image.style.maxWidth = '50px';
          image.style.maxHeight = '50px';
          imageCell.appendChild(image);
          // Tạo liên kết <a> bao quanh hình ảnh và mở hình ảnh trong một popup
          const link = document.createElement('a');
          link.href = item.imageUrl;
          link.target = '_blank';
          link.onclick = function(event) {
            event.preventDefault();
            window.open(item.imageUrl, 'popup', 'width=600,height=600');
          };  
          link.appendChild(image);
          imageCell.appendChild(link);  // Thêm liên kết chứa hình ảnh vào ô

        }
        row.appendChild(imageCell);

        const actionCell = document.createElement('td');
        const deleteButton = document.createElement('button');
        actionCell.style = 'text-align: center';
        deleteButton.className = 'delete-button';
        deleteButton.textContent = 'Delete';
        deleteButton.onclick = function() {
          deleteTodoItem(item.id);
        };
        actionCell.appendChild(deleteButton);
        row.appendChild(actionCell);

        table.appendChild(row);
      });

      list.appendChild(table);
    }

    document.addEventListener('DOMContentLoaded', function() {
      loadTodoItems();
    });
  </script>
</head>
<body>
  <div class="title">DANH SÁCH VIỆC LÀM</div>
  <div class="title1">Có gửi email nhắc việc</div>
    <div >
      <form class="div-form" id="todoForm" onsubmit="event.preventDefault(); addTodoItem();">    
        <div>
          <label for="title"><i class="bi bi-arrow-bar-right"></i> Tiêu đề:</label>
          <input type="text" id="title" required>
        </div>
        <div>
          <label for="description"><i class="bi bi-book"></i> Mô tả công việc</label>
          <textarea class="title-textarea" id="description" required></textarea>
        </div>
        <div >
          <label for="dueDate"><i class="bi bi-calendar-date"></i> Ngày công việc:</label>
          <input type="date" id="dueDate" required>
        </div>
        <div>
          <label for="image"><i class="bi bi-card-image"></i> Tải ảnh lên:</label>
          <input type="file" id="image" required>
        </div>
        <button class="bnt" type="submit">Thêm công việc</button>
      </form> 
    </div>
  <div id="todoList"></div>
</body>
</html>