Hướng dẫn

Cách Tạo Web App Từ Google Sheets Bằng Apps Script [Hướng Dẫn 2027]

Tuân HoangTuân Hoang
27 tháng 2, 2026
Cập nhật: 9 tháng 10, 2026
26 phút đọc
Ảnh minh họa bài viết: Cách Tạo Web App Từ Google Sheets Bằng Apps Script [Hướng Dẫn 2027]

Để tạo web app từ Google Sheets bằng Apps Script, bạn mở Extensions > Apps Script, viết hàm doGet() và doPost() trong Code.gs để xử lý dữ liệu, thiết kế giao diện bằng file HTML riêng, rồi bấm Deploy để lấy link. Toàn bộ quy trình miễn phí hosting, miễn phí SSL, không cần thuê developer — trong khi website thường tốn 50-500 nghìn đồng mỗi tháng cho hosting. Web app phù hợp làm form thu thập dữ liệu, dashboard nội bộ hoặc portal khách hàng, chỉ cần JavaScript và HTML cơ bản.

Web App Từ Google Sheets Là Gì Và Vì Sao Miễn Phí Cho Mọi Người?

Bạn có biết rằng Google Sheets không chỉ là bảng tính? Với Google Apps Script, bạn có thể biến bất kỳ spreadsheet nào thành một ứng dụng web hoàn chỉnh - có giao diện HTML đẹp mắt, xử lý form, hiển thị dữ liệu, thậm chí xây dựng cả hệ thống CRUD (Create, Read, Update, Delete) - và tất cả đều miễn phí hosting, miễn phí domain, miễn phí SSL.

Apps Script Web App là giải pháp tuyệt vời cho các tình huống:

  • Form thu thập dữ liệu đẹp hơn Google Forms, tuỳ chỉnh 100% giao diện
  • Dashboard nội bộ cho team tra cứu thông tin từ Sheets
  • Portal khách hàng để theo dõi đơn hàng, tra cứu bảo hành
  • Hệ thống chấm điểm, đánh giá với giao diện thân thiện
  • Landing page đơn giản với form liên hệ lưu vào Sheets
  • Internal tools cho doanh nghiệp mà không cần thuê developer

Ưu điểm Web App từ Google Sheets:

Tiêu chí Web App (Apps Script) Website thường
Chi phí hosting MIỄN PHÍ (Google server) 50-500K/thang
SSL (HTTPS) Có sẵn Phải cài đặt
Database Google Sheets (quen thuộc) MySQL, MongoDB...
Deploy 1 click CI/CD, FTP, Docker...
Authentication Google Account (có sẵn) Tự xây dựng
Yêu cầu kỹ thuật JavaScript + HTML cơ bản Full-stack developer

Bài viết này sẽ hướng dẫn bạn:

  • Hieu cấu trúc dự án Web App (Code.gs + HTML files)
  • Nắm vững doGet/doPost - 2 hàm cốt lõi của Web App
  • Sử dụng HtmlService để tạo giao diện đẹp
  • Xây dựng CRUD operations đầy đủ (Create/Read/Update/Delete)
  • Thực hành với 3 ví dụ thực tế kèm code mẫu
  • Biết cách deploy và chia sẻ Web App cho người khác dùng
  • Nắm rõ giới hạn và tips bảo mật quan trọng

Cấu Trúc Dự Án Web App Trong Apps Script Gồm Những Gì?

Một dự án Web App trong Apps Script gồm 3 thành phần chính:

Cấu trúc thư mục:

Apps Script Project/
  |-- Code.gs          (Server-side: doGet, doPost, xu ly data)
  |-- Page.html        (Client-side: giao dien nguoi dung)
  |-- Stylesheet.html  (CSS styles)
  |-- JavaScript.html  (Client-side JavaScript)
  |-- appsscript.json  (Manifest - cau hinh project)

Code.gs là file server-side, viết bằng JavaScript (chạy trên server Google). Đây là nơi bạn định nghĩa các hàm xử lý dữ liệu, đọc/ghi Google Sheets, và trả về HTML cho người dùng.

HTML files là giao diện người dùng. Bạn có thể tạo nhiều file .html cho các trang khác nhau. CSS và JavaScript của client cũng được viết trong file .html riêng (vì Apps Script không hỗ trợ file .css hay .js riêng).

appsscript.json là file cấu hình, định nghĩa quyền truy cập (scopes), runtime version, và các thiết lập khác.

Cách Setup Project Apps Script Như Thế Nào?

Bước 1: Mở Google Sheets và tạo Apps Script project

  1. Mở Google Sheets bất kỳ (hoặc tạo mới tại sheets.new)
  2. Vào menu Extensions > Apps Script
  3. Cửa sổ Script Editor sẽ mở ra với file Code.gs mặc định
  4. Đổi tên project (click vào "Untitled project" ở góc trái trên)

Bước 2: Tạo file HTML

  1. Trong Script Editor, click dấu + bên cạnh "Files"
  2. Chon HTML
  3. Đặt tên file (VD: "Page", "Index", "Dashboard"...)
  4. File .html sẽ được tạo với template HTML5 cơ bản

Luu y: Khi tạo file HTML, không cần thêm đuôi .html - Apps Script tự động thêm. VD: nhập "Page" sẽ tạo file "Page.html".

doGet Và doPost Trong Code.gs Hoạt Động Ra Sao?

doGet() va doPost() là 2 hàm đặc biệt trong Apps Script. Khi bạn deploy Web App:

  • doGet(e) được gọi khi người dùng truy cập URL bằng trình duyệt (HTTP GET)
  • doPost(e) được gọi khi có POST request (VD: submit form, webhook)

3.1 doGet - Hiển thị giao diện

// Code.gs - Ham doGet co ban
function doGet(e) {
  // Tra ve file HTML lam giao dien
  return HtmlService.createHtmlOutputFromFile('Page')
    .setTitle('Ung Dung Quan Ly')
    .setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL);
}

// Phien ban nang cao voi Template (cho phep truyen du lieu vao HTML)
function doGet(e) {
  var template = HtmlService.createTemplateFromFile('Page');

  // Truyen du lieu tu server vao HTML template
  template.pageTitle = 'Dashboard Quan Ly Don Hang';
  template.userName = Session.getActiveUser().getEmail();
  template.currentDate = Utilities.formatDate(new Date(), 'Asia/Ho_Chi_Minh', 'dd/MM/yyyy HH:mm');

  return template.evaluate()
    .setTitle('Dashboard')
    .setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL);
}

// Xu ly tham so URL (VD: ?page=dashboard&id=123)
function doGet(e) {
  var page = e.parameter.page || 'home';
  var template;

  if (page === 'dashboard') {
    template = HtmlService.createTemplateFromFile('Dashboard');
  } else if (page === 'form') {
    template = HtmlService.createTemplateFromFile('Form');
  } else {
    template = HtmlService.createTemplateFromFile('Home');
  }

  return template.evaluate()
    .setTitle('My App - ' + page)
    .setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL);
}

3.2 doPost - Nhận dữ liệu từ form hoặc webhook

// Code.gs - Ham doPost xu ly form submit
function doPost(e) {
  try {
    var data = JSON.parse(e.postData.contents);
    var ss = SpreadsheetApp.getActiveSpreadsheet();
    var sheet = ss.getSheetByName('Data');

    // Luu du lieu vao sheet
    sheet.appendRow([
      new Date(),
      data.name || '',
      data.email || '',
      data.phone || '',
      data.message || ''
    ]);

    return ContentService.createTextOutput(JSON.stringify({
      status: 'success',
      message: 'Du lieu da duoc luu thanh cong!'
    })).setMimeType(ContentService.MimeType.JSON);

  } catch (error) {
    return ContentService.createTextOutput(JSON.stringify({
      status: 'error',
      message: error.toString()
    })).setMimeType(ContentService.MimeType.JSON);
  }
}

3.3 HtmlService - Tạo giao diện

HtmlService cung cấp 3 cách tạo HTML:

// Cach 1: Tu file HTML
HtmlService.createHtmlOutputFromFile('Page')

// Cach 2: Tu string (cho HTML don gian)
HtmlService.createHtmlOutput('<h1>Xin chao!</h1>')

// Cach 3: Tu Template (KHUYEN DUNG - cho phep truyen du lieu)
var template = HtmlService.createTemplateFromFile('Page');
template.data = getSheetData();
template.evaluate()

Template syntax cho phép nhúng code Apps Script vào HTML:

<!-- Page.html -->
<!DOCTYPE html>
<html>
<head>
  <base target="_top">
  <style>
    body { font-family: Arial, sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; }
    .card { border: 1px solid #ddd; border-radius: 8px; padding: 16px; margin: 8px 0; }
    .btn { background: #1a73e8; color: white; border: none; padding: 10px 20px; border-radius: 6px; cursor: pointer; }
    .btn:hover { background: #1557b0; }
  </style>
</head>
<body>
  <h1>Dashboard</h1>
  <p>Xin chao! Hom nay la: <?= currentDate ?></p>

  <!-- Vong lap hien thi du lieu tu Sheets -->
  <? var data = getData(); ?>
  <? for (var i = 0; i < data.length; i++) { ?>
    <div class="card">
      <h3><?= data[i][0] ?></h3>
      <p>Email: <?= data[i][1] ?></p>
      <p>Trang thai: <?= data[i][2] ?></p>
    </div>
  <? } ?>

</body>
</html>

3.4 Include Pattern - Tách CSS và JS riêng

Để code sạch sẽ, tách CSS và JavaScript ra file riêng rồi include vào:

// Code.gs - Ham include file
function include(filename) {
  return HtmlService.createHtmlOutputFromFile(filename).getContent();
}

// Page.html - Su dung include
// <!DOCTYPE html>
// <html>
// <head>
//   <base target="_top">
//   <?!= include('Stylesheet') ?>
// </head>
// <body>
//   <h1>My App</h1>
//   <div id="content"></div>
//   <?!= include('JavaScript') ?>
// </body>
// </html>
<!-- Stylesheet.html -->
<style>
  * { box-sizing: border-box; margin: 0; padding: 0; }
  body {
    font-family: 'Segoe UI', Arial, sans-serif;
    background: #f5f5f5;
    color: #333;
  }
  .container { max-width: 960px; margin: 0 auto; padding: 20px; }
  .card {
    background: white;
    border-radius: 12px;
    box-shadow: 0 2px 8px rgba(0,0,0,0.1);
    padding: 20px;
    margin-bottom: 16px;
  }
  .btn-primary {
    background: #1a73e8;
    color: white;
    border: none;
    padding: 12px 24px;
    border-radius: 8px;
    font-size: 16px;
    cursor: pointer;
    transition: background 0.2s;
  }
  .btn-primary:hover { background: #1557b0; }
  .form-group { margin-bottom: 16px; }
  .form-group label { display: block; font-weight: 600; margin-bottom: 4px; }
  .form-group input, .form-group textarea, .form-group select {
    width: 100%;
    padding: 10px 12px;
    border: 1px solid #ddd;
    border-radius: 6px;
    font-size: 14px;
  }
  .table { width: 100%; border-collapse: collapse; }
  .table th, .table td { border: 1px solid #ddd; padding: 10px; text-align: left; }
  .table th { background: #f0f0f0; font-weight: 600; }
  .table tr:hover { background: #f9f9f9; }
  .badge { padding: 4px 10px; border-radius: 20px; font-size: 12px; font-weight: 600; }
  .badge-success { background: #d4edda; color: #155724; }
  .badge-warning { background: #fff3cd; color: #856404; }
  .badge-danger { background: #f8d7da; color: #721c24; }
</style>

Cách Thực Hiện CRUD Operations Trên Google Sheets?

Đây là phần quan trọng nhất. CRUD (Create, Read, Update, Delete) là 4 thao tác cơ bản để tương tác với dữ liệu trong Google Sheets từ Web App.

4.1 google.script.run - Cầu nối Client và Server

Trong Web App, client-side JavaScript giao tiếp với server-side Apps Script thông qua google.script.run:

// Client-side (trong file HTML)
// Goi ham server-side va xu ly ket qua
google.script.run
  .withSuccessHandler(function(result) {
    console.log('Thanh cong:', result);
    // Xu ly ket qua o day
  })
  .withFailureHandler(function(error) {
    console.error('Loi:', error);
    alert('Co loi xay ra: ' + error.message);
  })
  .getDataFromSheet(); // Ten ham trong Code.gs

4.2 CREATE - Thêm dữ liệu mới

// ====== SERVER-SIDE (Code.gs) ======
function addNewRecord(formData) {
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  var sheet = ss.getSheetByName('Data');

  // Tao ID tu dong
  var lastRow = sheet.getLastRow();
  var newId = lastRow > 0 ? 'ID-' + (lastRow + 1).toString().padStart(4, '0') : 'ID-0001';

  // Them dong moi
  sheet.appendRow([
    newId,
    formData.name,
    formData.email,
    formData.phone,
    formData.address,
    new Date(),  // Ngay tao
    'Active'     // Status mac dinh
  ]);

  return {
    success: true,
    id: newId,
    message: 'Da them thanh cong: ' + formData.name
  };
}
// ====== CLIENT-SIDE (JavaScript.html) ======
// <script>
function submitForm() {
  var formData = {
    name: document.getElementById('name').value,
    email: document.getElementById('email').value,
    phone: document.getElementById('phone').value,
    address: document.getElementById('address').value
  };

  // Validate
  if (!formData.name || !formData.email) {
    alert('Vui long dien ten va email!');
    return;
  }

  // Hien loading
  document.getElementById('submitBtn').disabled = true;
  document.getElementById('submitBtn').textContent = 'Dang xu ly...';

  google.script.run
    .withSuccessHandler(function(result) {
      if (result.success) {
        alert(result.message);
        document.getElementById('myForm').reset();
        loadData(); // Refresh bang du lieu
      }
      document.getElementById('submitBtn').disabled = false;
      document.getElementById('submitBtn').textContent = 'Them moi';
    })
    .withFailureHandler(function(error) {
      alert('Loi: ' + error.message);
      document.getElementById('submitBtn').disabled = false;
      document.getElementById('submitBtn').textContent = 'Them moi';
    })
    .addNewRecord(formData);
}
// </script>

4.3 READ - Đọc và hiển thị dữ liệu

// ====== SERVER-SIDE (Code.gs) ======
function getAllRecords() {
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  var sheet = ss.getSheetByName('Data');
  var data = sheet.getDataRange().getValues();

  if (data.length <= 1) return []; // Chi co header

  var records = [];
  var headers = data[0];

  for (var i = 1; i < data.length; i++) {
    var record = {};
    for (var j = 0; j < headers.length; j++) {
      record[headers[j]] = data[i][j];
    }
    records.push(record);
  }

  return records;
}

// Tim kiem theo dieu kien
function searchRecords(keyword) {
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  var sheet = ss.getSheetByName('Data');
  var data = sheet.getDataRange().getValues();
  var results = [];

  var keywordLower = keyword.toLowerCase();
  for (var i = 1; i < data.length; i++) {
    var rowStr = data[i].join(' ').toLowerCase();
    if (rowStr.indexOf(keywordLower) !== -1) {
      results.push({
        id: data[i][0],
        name: data[i][1],
        email: data[i][2],
        phone: data[i][3],
        status: data[i][6]
      });
    }
  }

  return results;
}

// Lay 1 record theo ID
function getRecordById(id) {
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  var sheet = ss.getSheetByName('Data');
  var data = sheet.getDataRange().getValues();

  for (var i = 1; i < data.length; i++) {
    if (data[i][0] === id) {
      return {
        id: data[i][0],
        name: data[i][1],
        email: data[i][2],
        phone: data[i][3],
        address: data[i][4],
        createdAt: data[i][5],
        status: data[i][6]
      };
    }
  }
  return null;
}
// ====== CLIENT-SIDE (JavaScript.html) ======
// <script>
function loadData() {
  document.getElementById('tableBody').innerHTML = '<tr><td colspan="6">Dang tai...</td></tr>';

  google.script.run
    .withSuccessHandler(function(records) {
      var html = '';
      if (records.length === 0) {
        html = '<tr><td colspan="6">Chua co du lieu</td></tr>';
      } else {
        for (var i = 0; i < records.length; i++) {
          var r = records[i];
          var badgeClass = r.status === 'Active' ? 'badge-success' : 'badge-danger';
          html += '<tr>'
            + '<td>' + r.id + '</td>'
            + '<td>' + r.name + '</td>'
            + '<td>' + r.email + '</td>'
            + '<td>' + r.phone + '</td>'
            + '<td><span class="badge ' + badgeClass + '">' + r.status + '</span></td>'
            + '<td>'
            + '<button onclick="editRecord(\'+ "'" + r.id + "'" + '\)" class="btn-sm">Sua</button> '
            + '<button onclick="deleteRecord(\'+ "'" + r.id + "'" + '\)" class="btn-sm btn-danger">Xoa</button>'
            + '</td>'
            + '</tr>';
        }
      }
      document.getElementById('tableBody').innerHTML = html;
      document.getElementById('recordCount').textContent = 'Tong: ' + records.length + ' ban ghi';
    })
    .withFailureHandler(function(error) {
      document.getElementById('tableBody').innerHTML = '<tr><td colspan="6">Loi: ' + error.message + '</td></tr>';
    })
    .getAllRecords();
}

// Goi khi trang tai xong
window.onload = loadData;
// </script>

4.4 UPDATE - Cập nhật dữ liệu

// ====== SERVER-SIDE (Code.gs) ======
function updateRecord(id, updatedData) {
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  var sheet = ss.getSheetByName('Data');
  var data = sheet.getDataRange().getValues();

  for (var i = 1; i < data.length; i++) {
    if (data[i][0] === id) {
      var rowNumber = i + 1; // Sheets rows bat dau tu 1
      sheet.getRange(rowNumber, 2).setValue(updatedData.name);
      sheet.getRange(rowNumber, 3).setValue(updatedData.email);
      sheet.getRange(rowNumber, 4).setValue(updatedData.phone);
      sheet.getRange(rowNumber, 5).setValue(updatedData.address);

      return { success: true, message: 'Da cap nhat: ' + id };
    }
  }

  return { success: false, message: 'Khong tim thay ID: ' + id };
}

4.5 DELETE - Xóa dữ liệu

// ====== SERVER-SIDE (Code.gs) ======
function deleteRecord(id) {
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  var sheet = ss.getSheetByName('Data');
  var data = sheet.getDataRange().getValues();

  for (var i = 1; i < data.length; i++) {
    if (data[i][0] === id) {
      sheet.deleteRow(i + 1);
      return { success: true, message: 'Da xoa: ' + id };
    }
  }

  return { success: false, message: 'Khong tim thay ID: ' + id };
}

// Soft delete (danh dau da xoa thay vi xoa han)
function softDeleteRecord(id) {
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  var sheet = ss.getSheetByName('Data');
  var data = sheet.getDataRange().getValues();

  for (var i = 1; i < data.length; i++) {
    if (data[i][0] === id) {
      sheet.getRange(i + 1, 7).setValue('Deleted');
      sheet.getRange(i + 1, 8).setValue(new Date());
      return { success: true, message: 'Da xoa (soft): ' + id };
    }
  }

  return { success: false, message: 'Khong tim thay ID: ' + id };
}

Cách Deploy Web App Từ Apps Script?

Sau khi viết code xong, bạn cần deploy để Web App hoạt động. Đây là hướng dẫn chi tiết:

Bước 1: Deploy lần đầu

  1. Trong Script Editor, click Deploy > New deployment
  2. Click icon bánh răng (gear) > chọn Web app
  3. Điền thông tin:
    • Description: Mô tả phiên bản (VD: "v1.0 - Initial release")
    • Execute as: Chọn "Me" (chạy bằng tài khoản của bạn)
    • Who has access: Chọn quyền truy cập (xem bảng dưới)
  4. Click Deploy
  5. Copy Web app URL - đây là link truy cập ứng dụng
Tùy chọn "Who has access" Mô tả Phù hợp cho
Only myself Chỉ mình bạn truy cập được Testing, tool cá nhân
Anyone with Google account Bất kỳ ai có Google account Ứng dụng nội bộ công ty
Anyone Bất kỳ ai (không cần đăng nhập) Form công khai, landing page

Bước 2: Cập nhật phiên bản

Khi bạn sửa code và muốn cập nhật Web App:

  1. Click Deploy > Manage deployments
  2. Click icon bút chì (edit) của deployment hiện tại
  3. O Version, chon New version
  4. Thêm mô tả phiên bản mới
  5. Click Deploy

Lưu ý quan trọng: Nếu bạn không chọn "New version", người dùng sẽ vẫn thấy phiên bản cũ! Đây là lỗi thường gặp nhất khi làm Web App. Luôn tạo New version khi muốn cập nhật.

Phần 6: 3 Ví Dụ Thực Tế Kèm Code Mẫu

Ví dụ 1: Form Đăng Ký Sự Kiện

Tạo form đăng ký sự kiện đẹp hơn Google Forms, tự động gửi email xác nhận, hiển thị số người đã đăng ký, và tự động đóng form khi hết slot.

// ====== Code.gs ======
var SHEET_NAME = 'Registrations';
var MAX_SLOTS = 50;
var EVENT_NAME = 'Workshop Google Sheets Nang Cao';
var EVENT_DATE = '15/03/2026 - 9:00 AM';

function doGet() {
  var template = HtmlService.createTemplateFromFile('EventForm');
  template.eventName = EVENT_NAME;
  template.eventDate = EVENT_DATE;
  template.slotsRemaining = getRemainingSlots();

  return template.evaluate()
    .setTitle('Dang Ky: ' + EVENT_NAME)
    .setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL);
}

function getRemainingSlots() {
  var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(SHEET_NAME);
  var registeredCount = Math.max(0, sheet.getLastRow() - 1); // Tru header
  return MAX_SLOTS - registeredCount;
}

function registerForEvent(formData) {
  var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(SHEET_NAME);
  var remaining = getRemainingSlots();

  if (remaining <= 0) {
    return { success: false, message: 'Rat tiec, su kien da het slot dang ky!' };
  }

  // Kiem tra trung email
  var data = sheet.getDataRange().getValues();
  for (var i = 1; i < data.length; i++) {
    if (data[i][2] === formData.email) {
      return { success: false, message: 'Email nay da dang ky roi!' };
    }
  }

  // Luu dang ky
  var registrationId = 'REG-' + new Date().getTime();
  sheet.appendRow([
    registrationId,
    formData.name,
    formData.email,
    formData.phone,
    formData.company || '',
    new Date(),
    'Confirmed'
  ]);

  // Gui email xac nhan
  try {
    MailApp.sendEmail({
      to: formData.email,
      subject: 'Xac nhan dang ky: ' + EVENT_NAME,
      htmlBody: '<div style="font-family: Arial; max-width: 600px; margin: 0 auto;">'
        + '<h2 style="color: #1a73e8;">Dang ky thanh cong!</h2>'
        + '<p>Xin chao <strong>' + formData.name + '</strong>,</p>'
        + '<p>Ban da dang ky thanh cong su kien <strong>' + EVENT_NAME + '</strong>.</p>'
        + '<p>Thoi gian: ' + EVENT_DATE + '</p>'
        + '<p>Ma dang ky: <strong>' + registrationId + '</strong></p>'
        + '<p>Vui long giu email nay de check-in tai su kien.</p>'
        + '<hr><p style="color: #666;">Gui tu SheetStore - sheet.com.vn</p>'
        + '</div>'
    });
  } catch(e) {
    Logger.log('Khong gui duoc email: ' + e.toString());
  }

  return {
    success: true,
    message: 'Dang ky thanh cong! Ma: ' + registrationId,
    remaining: remaining - 1
  };
}

File EventForm.html sẽ chứa giao diện form với các trường: Họ tên, Email, SĐT, Công ty. Hiển thị số slot còn lại và tự động disable form khi hết slot. Sử dụng CSS để tạo giao diện đẹp với progress bar hiển thị tỷ lệ đăng ký.

Ví dụ 2: Dashboard Tra Cứu Đơn Hàng

Tạo dashboard để khách hàng hoặc nhân viên tra cứu trạng thái đơn hàng chỉ bằng mã đơn.

// ====== Code.gs ======
function doGet(e) {
  var template = HtmlService.createTemplateFromFile('OrderDashboard');
  return template.evaluate()
    .setTitle('Tra Cuu Don Hang - SheetStore')
    .setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL);
}

function lookupOrder(orderNumber) {
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  var sheet = ss.getSheetByName('DonHang');
  var data = sheet.getDataRange().getValues();

  for (var i = 1; i < data.length; i++) {
    if (data[i][0] === orderNumber) {
      return {
        found: true,
        order: {
          orderNumber: data[i][0],
          customerName: data[i][1],
          orderDate: Utilities.formatDate(new Date(data[i][2]), 'Asia/Ho_Chi_Minh', 'dd/MM/yyyy HH:mm'),
          items: data[i][3],
          totalAmount: data[i][4],
          status: data[i][5],
          shippingAddress: data[i][6],
          trackingNumber: data[i][7] || 'Chua co',
          estimatedDelivery: data[i][8] ? Utilities.formatDate(new Date(data[i][8]), 'Asia/Ho_Chi_Minh', 'dd/MM/yyyy') : 'Dang cap nhat'
        }
      };
    }
  }

  return { found: false };
}

function getDashboardStats() {
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  var sheet = ss.getSheetByName('DonHang');
  var data = sheet.getDataRange().getValues();

  var stats = {
    total: data.length - 1,
    pending: 0,
    processing: 0,
    shipped: 0,
    delivered: 0,
    todayOrders: 0,
    todayRevenue: 0
  };

  var today = Utilities.formatDate(new Date(), 'Asia/Ho_Chi_Minh', 'yyyy-MM-dd');

  for (var i = 1; i < data.length; i++) {
    var status = data[i][5];
    if (status === 'Pending') stats.pending++;
    else if (status === 'Processing') stats.processing++;
    else if (status === 'Shipped') stats.shipped++;
    else if (status === 'Delivered') stats.delivered++;

    var orderDate = Utilities.formatDate(new Date(data[i][2]), 'Asia/Ho_Chi_Minh', 'yyyy-MM-dd');
    if (orderDate === today) {
      stats.todayOrders++;
      stats.todayRevenue += Number(data[i][4]) || 0;
    }
  }

  return stats;
}

Dashboard hiển thị: ô tìm kiếm nhập mã đơn, kết quả chi tiết với timeline trạng thái (Pending → Processing → Shipped → Delivered), thông tin vận chuyển, và các card thống kê tổng quan. Giao diện responsive trên mobile.

Ví dụ 3: Hệ Thống Đánh Giá (Rating System)

Tạo hệ thống đánh giá sản phẩm/dịch vụ với star rating, cho phép người dùng đánh giá và xem tổng hợp kết quả.

// ====== Code.gs ======
function doGet(e) {
  var template = HtmlService.createTemplateFromFile('RatingPage');
  template.items = getItemsWithRatings();
  return template.evaluate()
    .setTitle('Danh Gia San Pham')
    .setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL);
}

function getItemsWithRatings() {
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  var itemSheet = ss.getSheetByName('Items');
  var ratingSheet = ss.getSheetByName('Ratings');

  var items = itemSheet.getDataRange().getValues();
  var ratings = ratingSheet.getDataRange().getValues();

  var result = [];
  for (var i = 1; i < items.length; i++) {
    var itemId = items[i][0];
    var itemRatings = [];
    var totalScore = 0;

    for (var j = 1; j < ratings.length; j++) {
      if (ratings[j][1] === itemId) {
        itemRatings.push({
          userName: ratings[j][2],
          score: ratings[j][3],
          comment: ratings[j][4],
          date: ratings[j][5]
        });
        totalScore += Number(ratings[j][3]);
      }
    }

    var avgRating = itemRatings.length > 0 ? (totalScore / itemRatings.length).toFixed(1) : 0;

    result.push({
      id: itemId,
      name: items[i][1],
      description: items[i][2],
      image: items[i][3] || '',
      avgRating: avgRating,
      totalRatings: itemRatings.length,
      recentReviews: itemRatings.slice(-5).reverse() // 5 review moi nhat
    });
  }

  return result;
}

function submitRating(ratingData) {
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  var sheet = ss.getSheetByName('Ratings');

  // Validate
  if (!ratingData.itemId || !ratingData.score || !ratingData.userName) {
    return { success: false, message: 'Vui long dien day du thong tin!' };
  }

  if (ratingData.score < 1 || ratingData.score > 5) {
    return { success: false, message: 'Diem danh gia phai tu 1 den 5!' };
  }

  sheet.appendRow([
    'R-' + new Date().getTime(),
    ratingData.itemId,
    ratingData.userName,
    ratingData.score,
    ratingData.comment || '',
    new Date()
  ]);

  // Tinh lai trung binh
  var newAvg = calculateAverageRating(ratingData.itemId);

  return {
    success: true,
    message: 'Cam on ban da danh gia!',
    newAverage: newAvg
  };
}

function calculateAverageRating(itemId) {
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  var sheet = ss.getSheetByName('Ratings');
  var data = sheet.getDataRange().getValues();

  var total = 0;
  var count = 0;
  for (var i = 1; i < data.length; i++) {
    if (data[i][1] === itemId) {
      total += Number(data[i][3]);
      count++;
    }
  }

  return count > 0 ? (total / count).toFixed(1) : '0';
}

Giao diện hiển thị danh sách sản phẩm dạng card, mỗi card có: tên, mô tả, star rating (sao vàng), số lượng đánh giá, và nút "Đánh giá ngay". Khi click sẽ hiện modal với 5 sao để chọn và ô nhập nhận xét. Kết quả cập nhật realtime sau khi submit.

Cần Lưu Ý Gì Về Bảo Mật Cho Web App?

Web App từ Google Sheets là công cụ mạnh, nhưng cần chú ý bảo mật để tránh bị lạm dụng. Dưới đây là các tips quan trọng:

7.1 Validate Input Phía Server

KHÔNG BAO GIỜ chỉ validate phía client. Người dùng có thể bypass JavaScript và gửi dữ liệu trực tiếp lên server. Luôn validate lại trong Code.gs:

function addRecord(formData) {
  // 1. Kiem tra du lieu bat buoc
  if (!formData.name || !formData.email) {
    throw new Error('Thieu thong tin bat buoc');
  }

  // 2. Validate email format
  var emailRegex = /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+.[a-zA-Z]{2,}$/;
  if (!emailRegex.test(formData.email)) {
    throw new Error('Email khong hop le');
  }

  // 3. Validate do dai
  if (formData.name.length > 100 || formData.email.length > 100) {
    throw new Error('Du lieu qua dai');
  }

  // 4. Lam sach du lieu (chong XSS)
  formData.name = formData.name.replace(/</g, '').replace(/>/g, '');
  formData.email = formData.email.trim().toLowerCase();

  // 5. Luu vao sheet
  // ...
}

7.2 Giới Hạn Truy Cập

// Kiem tra quyen truy cap
function checkAccess() {
  var userEmail = Session.getActiveUser().getEmail();
  var allowedDomains = ['company.com', 'partner.com'];
  var allowedEmails = ['admin@gmail.com', 'manager@gmail.com'];

  // Kiem tra domain
  var userDomain = userEmail.split('@')[1];
  if (allowedDomains.indexOf(userDomain) !== -1) return true;

  // Kiem tra email cu the
  if (allowedEmails.indexOf(userEmail) !== -1) return true;

  return false;
}

function doGet(e) {
  if (!checkAccess()) {
    return HtmlService.createHtmlOutput(
      '<h1>Access Denied</h1><p>Ban khong co quyen truy cap ung dung nay.</p>'
    );
  }

  // Tiep tuc hien thi app...
  return HtmlService.createTemplateFromFile('Page').evaluate();
}

7.3 Rate Limiting

// Gioi han so request tu 1 nguoi trong 1 phut
function checkRateLimit(userEmail) {
  var cache = CacheService.getScriptCache();
  var key = 'rate_' + userEmail;
  var count = cache.get(key);

  if (count && Number(count) >= 30) { // Max 30 requests/phut
    throw new Error('Ban dang gui qua nhieu yeu cau. Vui long doi 1 phut.');
  }

  cache.put(key, count ? (Number(count) + 1).toString() : '1', 60); // TTL 60 giay
}

function addRecord(formData) {
  var userEmail = Session.getActiveUser().getEmail() || 'anonymous';
  checkRateLimit(userEmail);

  // Tiep tuc xu ly...
}

7.4 Logging và Audit Trail

// Ghi log moi thao tac
function logAction(action, details) {
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  var logSheet = ss.getSheetByName('AuditLog');
  if (!logSheet) {
    logSheet = ss.insertSheet('AuditLog');
    logSheet.appendRow(['Timestamp', 'User', 'Action', 'Details', 'IP']);
  }

  logSheet.appendRow([
    new Date(),
    Session.getActiveUser().getEmail() || 'anonymous',
    action,
    JSON.stringify(details),
    ''  // IP khong lay duoc trong Apps Script
  ]);
}

// Su dung
function deleteRecord(id) {
  logAction('DELETE', { recordId: id });
  // Thuc hien xoa...
}

Web App Từ Google Sheets Có Những Giới Hạn Gì?

Web App từ Google Sheets là công cụ tuyệt vời cho nhiều tình huống, nhưng không phải là giải pháp cho mọi thứ. Hiểu rõ giới hạn sẽ giúp bạn chọn công nghệ phù hợp.

Giới hạn Chi tiết Cách xử lý
Execution time: 6 phút Mỗi lần chạy script tối đa 6 phút. Quá thời gian sẽ bị timeout. Chia nhỏ tác vụ, dùng batch processing, pagination
Concurrent users hạn chế Khoảng 30 người dùng đồng thời. Nhiều hơn sẽ bị chậm hoặc lỗi. Cache kết quả, dùng CacheService, tối ưu query
Không có custom domain URL có dạng script.google.com/macros/... Không thể dùng tên miền riêng. Dùng iframe embed hoặc redirect từ domain riêng
Không hỗ trợ file upload trực tiếp Không thể dùng <input type="file"> bình thường. Dùng google.script.run với base64 encoding hoặc Google Picker API
Tốc độ phụ thuộc vào Google server Response time thường 1-3 giây cho request đơn giản. Sheets càng nhiều data càng chậm. Cache data, pagination, chỉ load dữ liệu cần thiết
Giới hạn API calls UrlFetchApp: 20,000 calls/ngày. MailApp: 100 emails/ngày (cá nhân). Batch requests, queue system, upgrade Google Workspace

Khi nào NÊN dùng Web App từ Sheets?

  • Internal tools cho team < 30 người
  • Form thu thập dữ liệu cần custom UI
  • Dashboard báo cáo nội bộ
  • Prototype nhanh trước khi xây ứng dụng chính thức
  • Landing page đơn giản với form liên hệ
  • Tool cá nhân (to-do list, habit tracker, expense tracker)

Khi nào KHÔNG nên dùng Web App từ Sheets?

  • Ứng dụng cần > 100 người dùng đồng thời
  • Cần tốc độ phản hồi < 500ms
  • Cần custom domain và SEO
  • E-commerce với giao dịch thanh toán
  • Ứng dụng mobile native
  • Dữ liệu nhạy cảm cần bảo mật cấp cao (y tế, tài chính)

Làm Sao Để Web App Chạy Nhanh Hơn?

Một số kỹ thuật giúp Web App của bạn chạy nhanh hơn và trải nghiệm người dùng tốt hơn:

9.1 Sử dụng CacheService

// Cache du lieu thuong truy cap (TTL 10 phut)
function getProductList() {
  var cache = CacheService.getScriptCache();
  var cachedData = cache.get('productList');

  if (cachedData) {
    return JSON.parse(cachedData);
  }

  // Neu khong co cache, doc tu sheet
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  var sheet = ss.getSheetByName('Products');
  var data = sheet.getDataRange().getValues();

  var products = [];
  for (var i = 1; i < data.length; i++) {
    products.push({
      id: data[i][0],
      name: data[i][1],
      price: data[i][2],
      stock: data[i][3]
    });
  }

  // Luu cache 10 phut (600 giay)
  cache.put('productList', JSON.stringify(products), 600);
  return products;
}

// Xoa cache khi data thay doi
function clearProductCache() {
  CacheService.getScriptCache().remove('productList');
}

9.2 Pagination - Phân trang dữ liệu

// Thay vi load toan bo, chi load tung trang
function getRecordsPage(pageNumber, pageSize) {
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  var sheet = ss.getSheetByName('Data');
  var totalRows = sheet.getLastRow() - 1; // Tru header
  var totalPages = Math.ceil(totalRows / pageSize);

  var startRow = (pageNumber - 1) * pageSize + 2; // +2 vi co header va Sheets bat dau tu 1
  var numRows = Math.min(pageSize, totalRows - (pageNumber - 1) * pageSize);

  if (numRows <= 0) return { records: [], totalPages: totalPages, currentPage: pageNumber };

  var range = sheet.getRange(startRow, 1, numRows, sheet.getLastColumn());
  var data = range.getValues();

  var records = [];
  for (var i = 0; i < data.length; i++) {
    records.push({
      id: data[i][0],
      name: data[i][1],
      email: data[i][2],
      status: data[i][3]
    });
  }

  return {
    records: records,
    totalPages: totalPages,
    currentPage: pageNumber,
    totalRecords: totalRows
  };
}

9.3 Loading State và UX

// Client-side: Hien thi loading spinner
// <script>
function showLoading() {
  document.getElementById('loading').style.display = 'flex';
  document.getElementById('content').style.opacity = '0.5';
}

function hideLoading() {
  document.getElementById('loading').style.display = 'none';
  document.getElementById('content').style.opacity = '1';
}

function loadData() {
  showLoading();
  google.script.run
    .withSuccessHandler(function(result) {
      hideLoading();
      renderTable(result);
    })
    .withFailureHandler(function(error) {
      hideLoading();
      showError(error.message);
    })
    .getAllRecords();
}

function showError(message) {
  var errorDiv = document.getElementById('error');
  errorDiv.textContent = message;
  errorDiv.style.display = 'block';
  setTimeout(function() { errorDiv.style.display = 'none'; }, 5000);
}

function showSuccess(message) {
  var successDiv = document.getElementById('success');
  successDiv.textContent = message;
  successDiv.style.display = 'block';
  setTimeout(function() { successDiv.style.display = 'none'; }, 3000);
}
// </script>

Phần 10: Câu Hỏi Thường Gặp (FAQ)

1. Web App có mất phí không?

Hoàn toàn miễn phí. Google Apps Script không tính phí hosting, SSL, hay bandwidth. Bạn chỉ cần tài khoản Google (Gmail miễn phí). Nếu dùng Google Workspace (trước đây là G Suite), bạn sẽ có giới hạn cao hơn (VD: 1500 emails/ngày thay vì 100).

2. Tôi có thể dùng framework CSS như Bootstrap, Tailwind trong Web App không?

Có! Bạn có thể include CSS framework qua CDN link trong file HTML. VD: thêm link Bootstrap CSS vào phần <head>. Tuy nhiên, nên dùng bản minified và chỉ include những component cần thiết để trang tải nhanh. Tailwind thì nên dùng CDN play version hoặc build trước rồi copy CSS vào.

3. Làm sao để debug khi Web App bị lỗi?

Server-side: dùng Logger.log() trong Code.gs, xem log tại Executions (trong Script Editor). Client-side: dùng console.log() và mở Developer Tools (F12) trong trình duyệt. Tip: deploy bản "Test" (click "Test deployments") để test nhanh mà không cần tạo version mới.

4. Web App có hoạt động trên mobile không?

Có, Web App chạy trên trình duyệt mobile bình thường. Tuy nhiên, bạn cần tự làm responsive design (dùng CSS media queries, flexbox, grid). Google không tự động tối ưu giao diện cho mobile. Khuyến nghị: dùng viewport meta tag và test trên nhiều kích thước màn hình.

5. Có thể kết nối Web App với nhiều Google Sheets khác nhau không?

Có. Dùng SpreadsheetApp.openByUrl() hoặc SpreadsheetApp.openById() để mở bất kỳ spreadsheet nào mà tài khoản của bạn có quyền truy cập. VD: Web App đọc dữ liệu từ 1 Sheets "Products", ghi vào Sheets "Orders" khác, và gửi email từ Gmail - tất cả trong cùng 1 script.

Tổng Kết

Web App từ Google Sheets bằng Apps Script là công cụ tuyệt vời để xây dựng ứng dụng web đơn giản mà không mất đồng nào cho hosting, domain, hay server. Với kiến thức HTML/CSS/JavaScript cơ bản và hiểu biết về Apps Script, bạn có thể tạo ra những tool nội bộ mạnh mẽ cho doanh nghiệp chỉ trong vài ngày.

Tóm tắt những gì bạn đã học:

  • 1. Cấu trúc dự án: Code.gs (server) + HTML files (client) + Stylesheet + JavaScript
  • 2. doGet/doPost: 2 hàm cốt lõi xử lý HTTP requests
  • 3. HtmlService: Tạo giao diện với Template syntax (<?= ?> và <? ?>)
  • 4. google.script.run: Cầu nối giữa client và server
  • 5. CRUD operations: Create, Read, Update, Delete dữ liệu trong Sheets
  • 6. Deploy: 1 click deploy với 3 mức quyền truy cập
  • 7. Bảo mật: Validate server-side, rate limiting, access control, audit log
  • 8. Tối ưu: CacheService, pagination, loading states

Hãy bắt đầu với ví dụ đơn giản nhất - một form thu thập dữ liệu. Khi đã quen với workflow, bạn sẽ nhanh chóng mở rộng thành dashboard, portal, và nhiều hơn nữa. Và khi nhu cầu vượt quá khả năng của Apps Script, bạn có thể chuyển sang các giải pháp chuyên nghiệp hơn mà vẫn giữ Google Sheets làm "database" quen thuộc.

Muốn Có Ngay Hệ Thống Quản Lý Chuyên Nghiệp?

Thay vì tự xây từng dòng code, hãy sử dụng phần mềm có sẵn với giao diện đẹp, tính năng đầy đủ, và hỗ trợ kỹ thuật

Khám phá SheetStore

Bài viết liên quan:

📖 Xem thêm: Google Sheets + Zalo OA: Tự Động Gửi Tin Nhắn 2026

Bài viết liên quan

Chia sẻ bài viết:

Tuân Hoang

Tuân Hoang

Đội ngũ SheetStore

Google SheetsGoogle Apps ScriptCRMAutomationPhần mềm quản lý doanh nghiệp

Google Workspace Certified, 5+ years experience

Bạn thấy bài viết hữu ích?

Đăng ký nhận thông báo khi có bài viết mới.

Nhận thông báo khi có bài viết mới. Không spam, hứa luôn! 😊

Bình luận (0)

Vui lòng đăng nhập để tham gia thảo luận