gpt51

가계부 · Wealth Ledger (VND)
Wealth Ledger
VND Real-time Household Account Book
Quick input Data integrity normal
VND
Total assets
₫0
This month's savings: ₫0
cash
₫0
Corporate account
₫0
personal account
₫0

Expense plan

Weekly spending plan
₫0 / ₫0
Monthly spending plan
₫0 / ₫0
VND
VND
Try increasing your savings by reducing actual spending compared to your plan.

Initial balance

VND
VND
VND
The initial balance is immediately reflected in the cumulative total.
It cannot be reversed. We recommend backing up.

Income/Expenses for the Last 12 Months

Monthly savings

Transaction history

dateaccountcategoryCategorymemoamountwork
0 case
import: ₫0
expenditure: ₫0
savings: ₫0
Habits build wealth. Record today as well ✨
VND
`; 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); } })();})();