가계부 · Wealth Ledger (VND)Buku Besar Kekayaan
Buku Catatan Keuangan Rumah Tangga VND Secara Real-time
Masukan cepat
Integritas data normal
Total aset
₫0
Penghematan bulan ini: ₫0
Rencana pengeluaran
Rencana pengeluaran mingguan
₫0 / ₫0
Rencana pengeluaran bulanan
₫0 / ₫0
Cobalah meningkatkan tabungan Anda dengan mengurangi pengeluaran aktual dibandingkan dengan rencana Anda.
Saldo awal
Saldo awal langsung tercermin dalam total kumulatif.
Ini tidak dapat dibatalkan. Kami menyarankan untuk melakukan pencadangan.
Pendapatan/Pengeluaran selama 12 Bulan Terakhir
Tabungan bulanan
| tanggal | akun | kategori | Kategori | memo | jumlah | bekerja |
|---|
`;
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);
}
})();})();