gpt51

가계부 · Wealth Ledger (VND)
Wealth Ledger
VND Real-time Household Account Book
Quick input Data integrity normal
VND
Total assets
₫0
Savings this month: ₫0
cash
₫0
corporate account
₫0
personal account
₫0

Spending plan

Weekly Spending Plan
₫0 / ₫0
Monthly spending plan
₫0 / ₫0
VND
VND
Increase your savings by reducing your actual spending compared to your plan.

Initial balance

VND
VND
VND
The initial balance is immediately reflected in the cumulative total.
This cannot be undone. We recommend that you make a backup.

Income/Expenses for the past 12 months

monthly savings

Transaction history

dateaccountcategoryCategorymemoamountwork
0 case
import: ₫0
expenditure: ₫0
savings: ₫0
Habits create wealth. Record them today too ✨
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); } })();})();