가계부 · Wealth Ledger (VND)Buku Besar Kekayaan
Buku Rekening Rumah Tangga Real-time VND
Masukan cepat
Integritas data normal
Total aset
Rp0
Penghematan bulan ini: Rp0
Rencana pengeluaran
Rencana Pengeluaran Mingguan
Rp0 / Rp0
Rencana pengeluaran bulanan
Rp0 / Rp0
Tingkatkan tabungan Anda dengan mengurangi pengeluaran aktual dibandingkan dengan rencana Anda.
Saldo awal
Saldo awal langsung tercermin dalam total kumulatif.
Ini tidak dapat dibatalkan. Kami sarankan Anda membuat cadangan.
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);
}
})();})();