Introduce
Trong thời đại số hóa, việc kiểm soát thu chi hàng ngày đòi hỏi sự nhanh chóng và chính xác. Thay vì nhập liệu thủ công trên những bảng tính Google Sheets phức tạp, tôi đã phát triển một giải pháp Web App tối ưu giúp việc quản lý tài chính trở nên đơn giản hơn bao giờ hết. Ứng dụng được xây dựng trên nền tảng Google Apps Script, sở hữu giao diện hiện đại, thân thiện và hoàn toàn tương thích với điện thoại di động. Điểm nổi bật của công cụ này là tính năng định dạng tiền tệ thông minh, tự động hiển thị dấu phân cách hàng nghìn ngay khi nhập, giúp loại bỏ hoàn toàn sai sót nhầm lẫn giữa các con số 0. Bên cạnh đó, hệ thống báo cáo tích hợp bộ lọc theo tháng cùng biểu đồ trực quan, cho phép người dùng theo dõi tổng thu, tổng chi và số dư chỉ với vài thao tác. Toàn bộ dữ liệu được đồng bộ hóa tức thì với Google Sheets, đảm bảo tính an toàn và bảo mật cao. Đây chính là trợ thủ đắc lực giúp bạn làm chủ kế hoạch tài chính cá nhân một cách chuyên nghiệp.
Key features
- Dùng Google sheet, Apps script miễn phí.
- Dễ dàng nhập liệu với Form hiện đại.
- Tự động tính toán và hiển thị.
- Cập nhật trên mọi thiết bị.
Source code (Snippet)
Below is the main Apps Script code for processing the data:
"function doGet() {
return HtmlService.createTemplateFromFile('Index')
.evaluate()
.setTitle('Quản Lý Thu Chi')
.addMetaTag('viewport', 'width=device-width, initial-scale=1');
}
const SPREADSHEET_ID = "" "";
function processForm(formData) {
const ss = SpreadsheetApp.openById(SPREADSHEET_ID);
const sheet = ss.getSheetByName(""Data"");
const id = ""ID-"" + new Date().getTime();
// Loại bỏ dấu chấm phân cách trước khi lưu vào Sheet để giữ định dạng số
const soTienThuan = formData.soTienDisplay.replace(/\./g, '');
const row = [
id,
formData.ngay,
formData.loai === ""thu"" ? soTienThuan : """",
formData.loai === ""thu"" ? formData.noiDung : """",
formData.loai === ""chi"" ? soTienThuan : """",
formData.loai === ""chi"" ? formData.noiDung : """"
];
sheet.appendRow(row);
return ""Đã lưu thành công!"";
}
function getDataByMonth(monthYear) {
try {
const ss = SpreadsheetApp.openById(SPREADSHEET_ID);
const sheet = ss.getSheetByName(""Data"");
const data = sheet.getDataRange().getValues();
if (data.length <= 1) return [];
data.shift();
const filtered = data.filter(row => {
if (!row[1]) return false;
try {
let formattedDate = Utilities.formatDate(new Date(row[1]), ""GMT+7"", ""yyyy-MM"");
return formattedDate === monthYear;
} catch(e) { return false; }
});
return filtered.map(row => [
row[0],
Utilities.formatDate(new Date(row[1]), ""GMT+7"", ""dd/MM/yy""),
row[2], row[3], row[4], row[5]
]);
} catch (e) { return []; }
}
<!DOCTYPE html>
<html>
<head>
<meta charset=""UTF-8"">
<link href=""https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css"" rel=""stylesheet"">
<script src=""https://cdn.jsdelivr.net/npm/chart.js""></script>
<style>
body { background: #f8f9fa; padding-top: 20px; font-family: sans-serif; }
.main-card { max-width: 500px; margin: auto; border-radius: 15px; border: none; box-shadow: 0 5px 15px rgba(0,0,0,0.1); }
.summary-box { background: #fff; padding: 15px; border-radius: 10px; border-left: 5px solid #0d6efd; }
.amount-input { font-size: 1.25rem; font-weight: bold; color: #d35400; }
</style>
</head>
<body>
<div class=""container"">
<div class=""card main-card p-4"">
<h4 class=""text-center mb-4"">💰 NHẬP THU CHI</h4>
<form id=""financeForm"">
<div class=""row g-3"">
<div class=""col-6"">
<label class=""form-label small"">Ngày</label>
<input type=""date"" name=""ngay"" id=""currentDate"" class=""form-control"" required>
</div>
<div class=""col-6"">
<label class=""form-label small"">Loại</label>
<select name=""loai"" class=""form-select"">
<option value=""thu"">Thu (+)</option>
<option value=""chi"">Chi (-)</option>
</select>
</div>
<div class=""col-12"">
<label class=""form-label small"">Số tiền (VNĐ)</label>
<input type=""text"" id=""soTienDisplay"" name=""soTienDisplay"" class=""form-control amount-input"" placeholder=""0"" required>
</div>
<div class=""col-12"">
<label class=""form-label small"">Nội dung</label>
<input type=""text"" name=""noiDung"" class=""form-control"" placeholder=""Lý do thu/chi..."">
</div>
<button type=""submit"" class=""btn btn-primary w-100 py-2 mt-3"">LƯU GIAO DỊCH</button>
</div>
</form>
<div id=""status"" class=""text-center mt-2 small""></div>
<hr>
<button class=""btn btn-outline-dark w-100"" onclick=""openFilter()"">📊 BÁO CÁO & LỌC DỮ LIỆU</button>
</div>
</div>
<div class=""modal fade"" id=""reportModal"" tabindex=""-1"">
<div class=""modal-dialog modal-lg"">
<div class=""modal-content"">
<div class=""modal-header"">
<h5 class=""modal-title"">Chi tiết theo tháng</h5>
<button type=""button"" class=""btn-close"" data-bs-dismiss=""modal""></button>
</div>
<div class=""modal-body"">
<div class=""input-group mb-3"">
<input type=""month"" id=""filterMonth"" class=""form-control"">
<button class=""btn btn-primary"" onclick=""loadData()"">Lọc</button>
</div>
<canvas id=""financeChart"" style=""max-height: 200px;"" class=""mb-4""></canvas>
<div id=""reportResult""></div>
</div>
</div>
</div>
</div>
<script src=""https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js""></script>
<script>
document.getElementById('currentDate').valueAsDate = new Date();
document.getElementById('filterMonth').value = new Date().toISOString().slice(0, 7);
let myChart = null;
// --- TỰ ĐỘNG ĐỊNH DẠNG SỐ TIỀN KHI GÕ ---
const amountInput = document.getElementById('soTienDisplay');
amountInput.addEventListener('input', function(e) {
let value = this.value.replace(/\D/g, """"); // Loại bỏ ký tự không phải số
if (value === """") {
this.value = """";
return;
}
// Định dạng số có dấu chấm phân cách hàng ngàn
this.value = parseInt(value).toLocaleString('de-DE');
});
function openFilter() {
new bootstrap.Modal(document.getElementById('reportModal')).show();
loadData();
}
function loadData() {
const month = document.getElementById('filterMonth').value;
google.script.run.withSuccessHandler(renderReport).getDataByMonth(month);
}
function renderReport(rows) {
if (!rows || rows.length === 0) {
document.getElementById('reportResult').innerHTML = '<p class=""text-center"">Không có dữ liệu.</p>';
return;
}
let tThu = 0, tChi = 0;
let html = `<table class=""table table-sm"" style=""font-size: 0.85rem;"">
<thead><tr><th>Ngày</th><th>Nội dung</th><th>Thu</th><th>Chi</th></tr></thead><tbody>`;
rows.forEach(r => {
let thu = parseFloat(r[2]) || 0; let chi = parseFloat(r[4]) || 0;
tThu += thu; tChi += chi;
let nd = thu > 0 ? r[3] : r[5]; // Lấy nội dung tương ứng
html += `<tr><td>${r[1]}</td><td>${nd}</td><td class=""text-success"">${thu?thu.toLocaleString():''}</td><td class=""text-danger"">${chi?chi.toLocaleString():''}</td></tr>`;
});
html += `</tbody></table>
<div class=""summary-box mt-3"">
<div class=""d-flex justify-content-between text-success small""><b>Thu:</b> <b>${tThu.toLocaleString()}đ</b></div>
<div class=""d-flex justify-content-between text-danger small""><b>Chi:</b> <b>${tChi.toLocaleString()}đ</b></div>
<div class=""d-flex justify-content-between mt-1""><b>Còn:</b> <b>${(tThu - tChi).toLocaleString()}đ</b></div>
</div>`;
document.getElementById('reportResult').innerHTML = html;
updateChart(tThu, tChi);
}
function updateChart(thu, chi) {
const ctx = document.getElementById('financeChart').getContext('2d');
if (myChart) myChart.destroy();
myChart = new Chart(ctx, {
type: 'pie', // Chuyển sang biểu đồ tròn cho dễ nhìn tỷ lệ
data: {
labels: ['Thu', 'Chi'],
datasets: [{ data: [thu, chi], backgroundColor: ['#198754', '#dc3545'] }]
},
options: { maintainAspectRatio: false }
});
}
document.getElementById('financeForm').onsubmit = function(e) {
e.preventDefault();
document.getElementById('status').innerText = ""Đang lưu..."";
google.script.run.withSuccessHandler(res => {
document.getElementById('status').innerText = ""✅ "" + res;
this.reset();
document.getElementById('currentDate').valueAsDate = new Date();
}).processForm(this);
};
</script>
</body>
</html>
"
Post a Comment