가계부 · Wealth Ledger (VND)Sổ ghi chép tài sản
Sổ tài khoản hộ gia đình thời gian thực VND
Nhập liệu nhanh
Tính toàn vẹn dữ liệu bình thường
Tổng tài sản
₫0
Tiết kiệm trong tháng này: ₫0
Kế hoạch chi phí
Kế hoạch chi tiêu hàng tuần
₫0 / ₫0
Kế hoạch chi tiêu hàng tháng
₫0 / ₫0
Hãy thử tăng khoản tiết kiệm của bạn bằng cách giảm chi tiêu thực tế so với kế hoạch ban đầu.
Số dư ban đầu
Số dư ban đầu được phản ánh ngay lập tức trong tổng số dư lũy kế.
Không thể khôi phục lại. Chúng tôi khuyên bạn nên sao lưu dữ liệu.
Thu nhập/Chi phí trong 12 tháng qua
Tiết kiệm hàng tháng
| ngày | tài khoản | loại | Loại | bản ghi nhớ | số lượng | công việc |
|---|
`;
const blob = new Blob([html], {type:'application/vnd.ms-excel'});
downloadBlob(blob, `ledger_vnd_${todayStr()}.xls`, 'application/vnd.ms-excel');
}
function backupJSON(){
const payload = {
settings, tx
};
const json = '\uFEFF'+JSON.stringify(payload);
downloadBlob(json, `ledger_vnd_backup_${todayStr()}.json`, 'application/json;charset=utf-8;');
}
function restoreJSON(e){
const f = e.target.files[0];
if(!f) return;
const reader = new FileReader();
reader.onload = function(){
try{
const obj = JSON.parse(reader.result);
if(!obj || !Array.isArray(obj.tx) || !obj.settings) throw new Error('bad file');
// minimal validation
settings = {
...defaultSettings(),
...obj.settings,
budgets: {
weekly: toInt(obj.settings?.budgets?.weekly||0),
monthly: toInt(obj.settings?.budgets?.monthly||0)
},
initBalances:{
cash: toInt(obj.settings?.initBalances?.cash||0),
biz: toInt(obj.settings?.initBalances?.biz||0),
personal: toInt(obj.settings?.initBalances?.personal||0),
}
};
tx = obj.tx.map(n=>({
id: n.id || uid(),
type: n.type==='in'?'in':'out',
account: ['cash','biz','personal'].includes(n.account)?n.account:'cash',
amount: toInt(n.amount||0),
date: n.date || fmtDateInput(new Date()),
category: n.category||'',
memo: n.memo||'',
createdAt: n.createdAt || Date.now()
}));
saveSettings(); saveTx();
// Refill inputs
els.langSelect.value = settings.lang;
applyI18N();
applyTheme(settings.theme);
setAmtInput(els.inpWeeklyPlan, settings.budgets.weekly);
setAmtInput(els.inpMonthlyPlan, settings.budgets.monthly);
setAmtInput(els.initCash, settings.initBalances.cash);
setAmtInput(els.initBiz, settings.initBalances.biz);
setAmtInput(els.initPersonal, settings.initBalances.personal);
drawAll();
showToast(t('imported'));
els.restoreFile.value = '';
}catch(err){
alert('Invalid backup file.');
}
};
reader.readAsText(f);
}// Helpers
function escCsv(s){
return (s||'').replace(/[\r\n]+/g,' ');
}
function csvCell(s){
if(s==null) return '';
const str = String(s);
if(/[",\n]/.test(str)) return `"${str.replace(/"/g,'""')}"`;
return str;
}
function downloadBlob(content, filename, mime){
let blob = content instanceof Blob ? content : new Blob([content], {type:mime});
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url; a.download = filename;
document.body.appendChild(a);
a.click();
setTimeout(()=>{ URL.revokeObjectURL(url); a.remove(); }, 100);
}
function todayStr(){
const d = new Date();
return `${d.getFullYear()}${String(d.getMonth()+1).padStart(2,'0')}${String(d.getDate()).padStart(2,'0')}`;
}
function uid(){
return 'id_'+Math.random().toString(36).slice(2,10)+Date.now().toString(36);
}
function escapeHtml(s){
return (s||'').replace(/[&<>"']/g, c=>({ '&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
}
function debounce(fn,ms){
let t; return (...a)=>{ clearTimeout(t); t=setTimeout(()=>fn(...a),ms); };
}
function alertI18n(key){ alert(t(key)); }// -------------- Lightweight self-test (doesn't change user data) --------------
// Ensures integrity math and formatter work. Runs on isolated data.
(function selfTest(){
try{
// pure compute with synthetic tx
const init = {cash: 1000, biz: 0, personal: 0};
const tempTx = [
{type:'in', account:'cash', amount: 2000, date:'2025-01-10'},
{type:'out', account:'cash', amount: 500, date:'2025-01-11'},
{type:'in', account:'biz', amount: 300, date:'2025-01-12'},
];
// compute
let c = {...init};
for(const r of tempTx){ c[r.account] += (r.type==='in'?1:-1)*r.amount; }
if(c.cash!==2500 || c.biz!==300 || c.personal!==0) throw new Error('balance mismatch');
// parse/format
if(parseAmt('1,234,567 VND')!==1234567) throw new Error('parseAmt failed');
formatVND(123); // should not throw
// ok
}catch(e){
console.warn('Self-test failed', e);
}
})();})();