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:
- Tạo hóa đơn trên Google sheet – Gửi email đính kèm hóa đơn pdf – Quản lý url hóa đơn trên danh sách
- Google sheet Ứng dụng theo dõi đơn hàng – Danh sách sản phẩm mua, trạng thái đơn hàng
- Tip on Ms Word Hướng dẫn tạo ngắt trang, xóa ngắt trang đơn lẽ hoặc toàn bộ ngắt trang trên file doc
- Google sheet | Generate QR Codes – Mã hóa nội dung Cell – Thêm hình ở giữa mã QR code
- Google sheet App Script | Tạo custom menu – Xóa tất cả dòng Rỗng trên vùng dữ liệu Nhanh chống
- Google Script Web | From upload file – Kiểm tra trùng lặp – Ghi đè hoặc Tạo phiên bản mới cho file
- Google Script Web | Trang Tìm kiếm nhiều điều kiên riêng lẽ hoặc hết hợp – Print, Xuất file PDF
- Hướng dẫn kiểm tra Bàn phím, Tháo và thay bàn phím mới cho Dell Inspirion 15 3000 series
- Google sheet webapp | Quản lý bán hàng – Chức nằn trả góp, In thông tin
- Ổ C đầy Di chuyển dữ liệu ZALO sang Ổ D hoặc ổ khác một cách dễ dàng Không lo zalo báo đầy ổ đĩa
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>