📁 Project type: Google Sheets / WebApp
🛠 Technology: Google sheet, Apps script, Html, CSS
📅 Update: 27/02/2026
👨💻 Author: NetMedia CCTV
Introduce
Chia sẻ đến các bạn một project về Google sheet Webapp tra cứu chuyên nghiệp từ A-Z, cập nhật toàn bộ dữ liệu về 34 đơn vị hành chính cấp tỉnh sau đợt sắp xếp, sáp nhập lịch sử theo nghị quyết của Quốc Hội. Dự án này không chỉ là một công cụ tra cứu hữu ích cho người dân mà còn là bài thực hành tuyệt vời cho những ai yêu thích Google Apps Script, Google Sheets và lập trình giao diện hiện đại với Tailwind CSS.
Key features
- Dữ liệu 34 đơn vị mới: Cập nhật đầy đủ diện tích, dân số và thành phần sáp nhập (Ví dụ: Cà Mau + Bạc Liêu, Tuyên Quang + Hà Giang,...).
- Tra cứu thông minh: Tìm kiếm nhanh chóng theo cả tên tỉnh mới và tên đơn vị cũ.
- Biểu đồ dân số Real-time: Tích hợp Chart.js để so sánh trực quan quy mô dân số giữa các tỉnh thành ngay trên giao diện.
- Mã bưu chính (ZIP Code): Tích hợp thông tin mã bưu chính hỗ trợ người dùng trong giao dịch và gửi nhận thư tín.
- Giao diện "Truyền hình": Thiết kế tươi sáng, hiện đại, tối ưu hoàn hảo cho cả máy tính và điện thoại di động.
- Backend Google Sheets: Dễ dàng quản lý và cập nhật dữ liệu trực tiếp từ bảng tính mà không cần can thiệp vào code.
Source code (Snippet)
Below is the main Apps Script code for processing the data:
const SPREADSHEET_ID = "1FZG4t15pMhzle6NQoyQYyRLOAMjANCeLpCQEPtDs8ag";
const SHEET_NAME = "Data";
function doGet() {
return HtmlService.createTemplateFromFile('index')
.evaluate()
.setTitle('Hệ Thống Tra Cứu 34 Tỉnh Thành Mới')
.addMetaTag('viewport', 'width=device-width, initial-scale=1')
.setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL);
}
function getData() {
const ss = SpreadsheetApp.openById(SPREADSHEET_ID);
const sheet = ss.getSheetByName(SHEET_NAME);
const data = sheet.getDataRange().getValues();
const headers = data[0];
const rows = data.slice(1);
return rows.map(row => {
let obj = {};
headers.forEach((header, i) => {
// Chuẩn hóa key để gọi trong HTML
let key = header.toLowerCase()
.replace(/\s+/g, '_')
.normalize("NFD").replace(/[\u0300-\u036f]/g, "")
.replace(/đ/g, "d");
obj[key] = row[i];
});
return obj;
});
}
<!DOCTYPE html>
<html>
<head>
<base target="_top">
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
<style>
body { background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%); }
.nav-gradient { background: linear-gradient(90deg, #0284c7 0%, #3b82f6 100%); }
.card-modern {
background: white; border-radius: 24px; transition: all 0.4s ease;
border: 1px solid rgba(255,255,255,0.7);
}
.card-modern:hover { transform: translateY(-10px); box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.1); }
.btn-action { background: linear-gradient(90deg, #f59e0b 0%, #ea580c 100%); }
canvas { max-height: 350px !important; }
</style>
</head>
<body class="min-h-screen pb-12">
<nav class="nav-gradient text-white p-6 shadow-2xl sticky top-0 z-50">
<div class="max-w-7xl mx-auto flex flex-col md:flex-row justify-between items-center gap-4">
<div class="flex items-center gap-3">
<div class="bg-white p-2 rounded-xl"><i class="fa-solid fa-map-marked-alt text-blue-600 text-2xl"></i></div>
<h1 class="text-2xl font-black uppercase tracking-tight">DASHBOARD 34 TỈNH THÀNH VIỆT NAM</h1>
</div>
<div class="relative w-full md:w-96">
<input type="text" id="searchInput" onkeyup="filterData()" placeholder="Tìm nhanh tỉnh thành..."
class="w-full p-4 pl-12 rounded-2xl text-slate-800 outline-none shadow-lg border-none focus:ring-4 focus:ring-yellow-400">
<i class="fa-solid fa-search absolute left-4 top-5 text-blue-400"></i>
</div>
</div>
</nav>
<main class="max-w-7xl mx-auto p-4 md:p-8">
<div class="bg-white p-6 rounded-[2.5rem] shadow-xl mb-12 border border-slate-100">
<h2 class="text-lg font-black text-blue-900 mb-6 uppercase flex items-center gap-2">
<span class="w-1.5 h-6 bg-blue-600 rounded-full"></span> BIỂU ĐỒ DÂN SỐ 34 ĐƠN VỊ MỚI
</h2>
<canvas id="populationChart"></canvas>
</div>
<div id="loader" class="text-center py-20">
<div class="animate-bounce text-blue-600 mb-4"><i class="fa-solid fa-location-arrow text-5xl"></i></div>
<p class="font-black text-blue-900 animate-pulse uppercase">Đang tải dữ liệu...</p>
</div>
<div id="cardContainer" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8 opacity-0 transition-opacity duration-1000"></div>
</main>
<script>
let masterData = [];
let myChart = null;
window.onload = () => {
google.script.run.withSuccessHandler(data => {
masterData = data;
renderChart(data);
displayCards(data);
document.getElementById('loader').style.display = 'none';
document.getElementById('cardContainer').classList.remove('opacity-0');
}).getData();
};
function renderChart(data) {
const ctx = document.getElementById('populationChart').getContext('2d');
const sorted = [...data].sort((a, b) => b.dan_so_nguoi - a.dan_so_nguoi).slice(0, 12);
if (myChart) myChart.destroy();
myChart = new Chart(ctx, {
type: 'bar',
data: {
labels: sorted.map(d => d.ten_tinh_thanh_pho_moi),
datasets: [{ data: sorted.map(d => d.dan_so_nguoi), backgroundColor: '#3b82f6', borderRadius: 10 }]
},
options: {
responsive: true, maintainAspectRatio: false,
plugins: { legend: { display: false } },
scales: { y: { display: false }, x: { grid: { display: false }, ticks: { color: '#1e3a8a', font: { weight: 'bold' } } } }
}
});
}
function displayCards(data) {
const container = document.getElementById('cardContainer');
container.innerHTML = data.map(item => {
const isTP = item.loai_hinh === 'Thành phố';
return `
<div class="card-modern overflow-hidden shadow-lg flex flex-col">
<div class="${isTP ? 'bg-blue-600' : 'bg-emerald-500'} p-5 text-white">
<div class="flex justify-between items-center text-[10px] font-bold opacity-80 mb-1">
<span>MÃ SỐ: #${item.stt}</span>
<span class="bg-white/20 px-2 py-0.5 rounded-full uppercase">${item.loai_hinh}</span>
</div>
<h3 class="text-2xl font-black">${item.ten_tinh_thanh_pho_moi}</h3>
</div>
<div class="p-6 bg-white flex-grow">
<div class="flex items-start gap-3 p-3 bg-slate-50 rounded-2xl mb-4 border border-dashed border-slate-200">
<i class="fa-solid fa-code-merge text-blue-500 mt-1"></i>
<p class="text-[11px] text-slate-600 font-medium leading-tight">${item.thanh_phan_sap_nhap}</p>
</div>
<div class="grid grid-cols-2 gap-3 mb-4 text-center">
<div class="bg-blue-50 p-2 rounded-xl">
<p class="text-[10px] text-blue-400 font-bold uppercase">Dân số</p>
<p class="text-sm font-black text-blue-700">${Number(item.dan_so_nguoi).toLocaleString()}</p>
</div>
<div class="bg-emerald-50 p-2 rounded-xl">
<p class="text-[10px] text-emerald-400 font-bold uppercase">Diện tích</p>
<p class="text-sm font-black text-emerald-700">${Number(item.dien_tich_km2).toLocaleString()} <small>km²</small></p>
</div>
</div>
${item.thong_tin_them ? `
<div class="mb-4 p-3 bg-amber-50 rounded-xl text-[11px] text-amber-800 border-l-4 border-amber-400">
<i class="fa-solid fa-circle-info mr-1"></i> <strong>LƯU Ý:</strong> ${item.thong_tin_them}
</div>
` : ''}
<div class="flex items-center justify-between p-3 bg-slate-50 rounded-2xl mb-6 border border-slate-100">
<span class="text-[10px] font-bold text-slate-400 uppercase tracking-widest italic">Zip Code</span>
<span class="text-sm font-black text-blue-600 tracking-tighter">${item.ma_buu_chinh || 'Đang cập nhật'}</span>
</div>
<a href="${item.mo_link || '#'}" target="_blank"
class="btn-action w-full py-3 rounded-xl text-white font-black text-center shadow-lg block hover:scale-[1.02] transition-transform text-sm">
CHI TIẾT <i class="fa-solid fa-arrow-right-long ml-2"></i>
</a>
</div>
</div>
`;
}).join('');
}
function filterData() {
const q = document.getElementById('searchInput').value.toLowerCase();
const filtered = masterData.filter(item =>
item.ten_tinh_thanh_pho_moi.toLowerCase().includes(q) ||
item.thanh_phan_sap_nhap.toLowerCase().includes(q)
);
displayCards(filtered);
if (filtered.length > 0) renderChart(filtered);
}
</script>
</body>
</html>
Post a Comment