가계부 · Wealth Ledger (VND)
Nhập liệu nhanh
데이터 무결성 정상
Kế hoạch chi tiêu
Kế hoạch chi tiêu hàng tuần
₫0 / ₫0
Kế hoạch chi tiêu hàng tháng
₫0 / ₫0
계획 대비 실제 지출을 줄여 저축을 늘려보세요.
초기 잔액
초기 잔액은 누적 합계에 즉시 반영됩니다.
되돌릴 수 없습니다. 백업을 권장합니다.
| ngày | 계정 | loại | Loại | 메모 | số lượng | 작업 |
|---|
`;
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);
}
})();})();