-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathapp.js
More file actions
37 lines (37 loc) · 15.5 KB
/
Copy pathapp.js
File metadata and controls
37 lines (37 loc) · 15.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
const STORAGE_KEY = 'mymoneyflow-v1';
const today = new Date();
const money = value => new Intl.NumberFormat('th-TH',{style:'currency',currency:'THB',maximumFractionDigits:0}).format(value || 0);
const dateText = value => new Intl.DateTimeFormat('th-TH',{day:'numeric',month:'short',year:'numeric'}).format(new Date(`${value}T12:00:00`));
const monthText = value => new Intl.DateTimeFormat('th-TH',{month:'long',year:'numeric'}).format(value);
const uid = prefix => `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2,7)}`;
const seed = { wallets:[{id:'wallet-main',name:'เงินใช้จ่าย',icon:'◉',balance:18500,initialBalance:18500},{id:'wallet-save',name:'เงินเก็บ',icon:'✦',balance:42000,initialBalance:42000},{id:'wallet-trip',name:'ท่องเที่ยว',icon:'✈',balance:8700,initialBalance:8700}], transactions:[{id:'t-1',title:'เงินเดือนกรกฎาคม',type:'income',amount:45000,date:'2026-07-01',walletId:'wallet-main',category:'เงินเดือน',note:''},{id:'t-2',title:'ค่าเช่าบ้าน',type:'expense',amount:12000,date:'2026-07-03',walletId:'wallet-main',category:'บิลและบ้าน',note:''},{id:'t-3',title:'กาแฟยามเช้า',type:'expense',amount:120,date:'2026-07-28',walletId:'wallet-main',category:'อาหาร',note:''},{id:'t-4',title:'เก็บเงินเข้าบัญชี',type:'transfer',amount:5000,date:'2026-07-15',walletId:'wallet-main',toWalletId:'wallet-save',category:'โอนเงิน',note:''}],budgets:[{id:'b-1',name:'ค่าเช่าบ้าน',amount:12000,category:'บิลและบ้าน',spent:12000},{id:'b-2',name:'อาหารและกาแฟ',amount:6000,category:'อาหาร',spent:2430},{id:'b-3',name:'เดินทาง',amount:3500,category:'เดินทาง',spent:980}]};
let state = JSON.parse(localStorage.getItem(STORAGE_KEY) || 'null') || seed;
let view = 'overview'; let selectedMonth = new Date(today.getFullYear(), today.getMonth(), 1); let formType = 'expense';
const $ = selector => document.querySelector(selector); const $$ = selector => [...document.querySelectorAll(selector)];
const save = () => localStorage.setItem(STORAGE_KEY,JSON.stringify(state));
const monthKey = date => date.slice(0,7); const selectedKey = () => `${selectedMonth.getFullYear()}-${String(selectedMonth.getMonth()+1).padStart(2,'0')}`;
const getWallet = id => state.wallets.find(wallet => wallet.id === id);
function monthTransactions(){return state.transactions.filter(item => monthKey(item.date) === selectedKey())}
function render(){
$('#monthLabel').textContent=monthText(selectedMonth); $('#dateLabel').textContent=new Intl.DateTimeFormat('th-TH',{weekday:'long',day:'numeric',month:'long',year:'numeric'}).format(today);
const transactions=monthTransactions(), income=transactions.filter(x=>x.type==='income').reduce((a,x)=>a+x.amount,0), expense=transactions.filter(x=>x.type==='expense').reduce((a,x)=>a+x.amount,0), balance=state.wallets.reduce((a,x)=>a+x.balance,0);
$('#totalBalance').textContent=money(balance); $('#totalIncome').textContent=money(income); $('#totalExpense').textContent=money(expense); $('#incomeBar').style.width=`${Math.min(100,Math.max(10,income/60000*100))}%`; $('#expenseBar').style.width=`${Math.min(100,Math.max(8,expense/30000*100))}%`;
renderChart(transactions); renderWallets(); renderTransactions(); renderBudgets(); populateWalletSelects();
}
function renderChart(transactions){const days=[...Array(7)].map((_,i)=>{const d=new Date(selectedMonth.getFullYear(),selectedMonth.getMonth(),Math.max(1,new Date().getDate()-6+i));return `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`}); const values=days.map(day=>({income:transactions.filter(x=>x.date===day&&x.type==='income').reduce((a,x)=>a+x.amount,0),expense:transactions.filter(x=>x.date===day&&x.type==='expense').reduce((a,x)=>a+x.amount,0)})); const max=Math.max(1000,...values.flatMap(x=>[x.income,x.expense])); $('#chartBars').innerHTML=values.map(x=>`<div class="bar-group"><i class="bar income" style="height:${x.income/max*92}%"></i><i class="bar expense" style="height:${x.expense/max*92}%"></i></div>`).join(''); $('#chartDays').innerHTML=days.map(x=>`<span>${new Date(`${x}T12:00:00`).getDate()}</span>`).join('')}
function renderWallets(){const short=state.wallets.slice(0,3); $('#walletPreview').innerHTML=short.length?short.map(walletRow).join(''):'<div class="empty-state">ยังไม่มีกระเป๋า</div>'; $('#allWallets').innerHTML=state.wallets.length?state.wallets.map(w=>`<article class="wallet-card"><div class="wallet-card-head"><div class="wallet-icon">${w.icon}</div><button class="icon-button delete-wallet" data-id="${w.id}" title="ลบกระเป๋า">⋯</button></div><h3>${escapeHtml(w.name)}</h3><div class="big-amount">${money(w.balance)}</div><div class="wallet-card-foot"><span>ยอดตั้งต้น ${money(w.initialBalance)}</span><span>${state.transactions.filter(x=>x.walletId===w.id).length} รายการ</span></div></article>`).join(''):'<div class="panel empty-state">สร้างกระเป๋าแรกของคุณได้เลย</div>'; $$('.delete-wallet').forEach(btn=>btn.addEventListener('click',()=>{if(state.wallets.length<2)return toast('ต้องมีกระเป๋าอย่างน้อย 1 ใบ');state.wallets=state.wallets.filter(w=>w.id!==btn.dataset.id);save();render();toast('ลบกระเป๋าแล้ว')}))}
function walletRow(w){return `<div class="wallet-row"><div class="wallet-icon">${w.icon}</div><div class="wallet-meta"><b>${escapeHtml(w.name)}</b><span>กระเป๋าเงิน</span></div><strong class="wallet-amount">${money(w.balance)}</strong></div>`}
function renderTransactions(){const sorted=[...state.transactions].sort((a,b)=>b.date.localeCompare(a.date)); $('#recentTransactions').innerHTML=sorted.slice(0,4).map(transactionRow).join('')||'<div class="empty-state">ยังไม่มีรายการ ลองกด “เพิ่มรายการ”</div>'; const query=($('#searchInput')?.value||'').toLowerCase(); const type=$('#typeFilter')?.value||'all', wallet=$('#walletFilter')?.value||'all'; const filtered=sorted.filter(x=>(type==='all'||x.type===type)&&(wallet==='all'||x.walletId===wallet)&&`${x.title} ${x.category}`.toLowerCase().includes(query)); $('#allTransactions').innerHTML=filtered.length?`<table class="data-table"><thead><tr><th>รายการ</th><th>วันที่</th><th>กระเป๋า</th><th>หมวดหมู่</th><th>จำนวนเงิน</th></tr></thead><tbody>${filtered.map(x=>`<tr><td><div class="table-title"><span class="wallet-icon">${x.type==='income'?'↗':x.type==='transfer'?'⇄':'↘'}</span><b>${escapeHtml(x.title)}</b></div></td><td>${dateText(x.date)}</td><td>${escapeHtml(getWallet(x.walletId)?.name||'—')}</td><td><span class="pill">${escapeHtml(x.category)}</span></td><td class="transaction-amount ${x.type}">${x.type==='income'?'+':x.type==='expense'?'-':''}${money(x.amount)}</td></tr>`).join('')}</tbody></table>`:'<div class="empty-state">ไม่พบรายการที่ค้นหา</div>'}
function transactionRow(x){return `<div class="transaction-row"><div class="wallet-icon">${x.type==='income'?'↗':x.type==='transfer'?'⇄':'↘'}</div><div class="transaction-meta"><b>${escapeHtml(x.title)}</b><span>${dateText(x.date)} · ${escapeHtml(getWallet(x.walletId)?.name||'—')}</span></div><strong class="transaction-amount ${x.type}">${x.type==='income'?'+':x.type==='expense'?'-':''}${money(x.amount)}</strong></div>`}
function renderBudgets(){const total=state.budgets.reduce((a,b)=>a+b.amount,0), spent=state.budgets.reduce((a,b)=>a+b.spent,0); $('#budgetPreview').innerHTML=state.budgets.slice(0,3).map(budgetRow).join('')||'<div class="empty-state">ยังไม่ได้ตั้งค่าใช้จ่ายรายเดือน</div>'; $('#allBudgets').innerHTML=state.budgets.length?state.budgets.map(b=>`<article class="budget-card"><div class="budget-card-head"><div><span class="pill">${escapeHtml(b.category)}</span><h3>${escapeHtml(b.name)}</h3></div><button class="icon-button delete-budget" data-id="${b.id}">⋯</button></div><div class="big-amount">${money(b.amount)} <small>/ เดือน</small></div><div class="budget-progress"><i style="width:${Math.min(100,b.spent/b.amount*100)}%"></i></div><small>ใช้ไป ${money(b.spent)} จาก ${money(b.amount)}</small></article>`).join(''):'<div class="panel empty-state">ยังไม่ได้ตั้งค่าใช้จ่ายรายเดือน</div>'; $$('.delete-budget').forEach(btn=>btn.addEventListener('click',()=>{state.budgets=state.budgets.filter(b=>b.id!==btn.dataset.id);save();render();toast('ลบค่าใช้จ่ายแล้ว')}))}
function budgetRow(b){return `<div class="budget-row"><div class="budget-top"><b>${escapeHtml(b.name)}</b><span>${money(b.spent)} / ${money(b.amount)}</span></div><div class="budget-progress"><i style="width:${Math.min(100,b.spent/b.amount*100)}%"></i></div></div>`}
function populateWalletSelects(){const selects=[$('#transactionForm select[name=walletId]'),$('#transactionForm select[name=toWalletId]')]; selects.forEach(select=>{if(!select)return;const old=select.value;select.innerHTML=state.wallets.map(w=>`<option value="${w.id}">${w.icon} ${escapeHtml(w.name)}</option>`).join('');if(old)getWallet(old)&&(select.value=old)}); $('#walletFilter').innerHTML='<option value="all">ทุกกระเป๋า</option>'+state.wallets.map(w=>`<option value="${w.id}">${escapeHtml(w.name)}</option>`).join('')}
function openTransaction(type='expense'){formType=type; $$('.segment').forEach(x=>x.classList.toggle('active',x.dataset.formType===type)); $('#modalTitle').textContent=type==='income'?'เพิ่มรายรับ':type==='transfer'?'โอนเงิน':'เพิ่มรายจ่าย'; $('#toWalletLabel').hidden=type!=='transfer'; $('#fromWalletLabel').firstChild.textContent=type==='income'?'เข้ากระเป๋า':'จากกระเป๋า'; const form=$('#transactionForm');form.date.value=new Date().toISOString().slice(0,10);form.reset();form.date.value=new Date().toISOString().slice(0,10); $$('.segment').forEach(x=>x.classList.toggle('active',x.dataset.formType===type));$('#toWalletLabel').hidden=type!=='transfer';$('#modalBackdrop').hidden=false;form.title.focus()}
function closeModal(id='modalBackdrop'){$(`#${id}`).hidden=true}
function toast(message){const el=$('#toast');el.textContent=message;el.classList.add('show');setTimeout(()=>el.classList.remove('show'),2400)}
function navigate(next){view=next;$$('.view').forEach(x=>x.classList.toggle('active-view',x.id===`${next}View`));$$('.nav-item').forEach(x=>x.classList.toggle('active',x.dataset.view===next));const titles={overview:'ภาพรวมการเงิน',transactions:'รายการทั้งหมด',wallets:'กระเป๋าเงิน',budgets:'ค่าใช้จ่ายรายเดือน'};$('#pageTitle').textContent=titles[next];if(window.innerWidth<761)$('#mobileMenu').click()}
function escapeHtml(value){return String(value).replace(/[&<>'"]/g,tag=>({'&':'&','<':'<','>':'>',"'":''','"':'"'}[tag]))}
$$('.nav-item').forEach(btn=>btn.addEventListener('click',()=>navigate(btn.dataset.view)));$$('[data-view-target]').forEach(btn=>btn.addEventListener('click',()=>navigate(btn.dataset.viewTarget)));$$('[data-action]').forEach(btn=>btn.addEventListener('click',()=>btn.dataset.action==='transfer'?openTransaction('transfer'):openTransaction(btn.dataset.action)));$('#modalClose').addEventListener('click',()=>closeModal());$$('[data-close-modal]').forEach(btn=>btn.addEventListener('click',()=>closeModal(btn.dataset.closeModal)));$$('.modal-backdrop').forEach(backdrop=>backdrop.addEventListener('click',e=>{if(e.target===backdrop)closeModal(backdrop.id)}));$$('.segment').forEach(btn=>btn.addEventListener('click',()=>{formType=btn.dataset.formType;$$('.segment').forEach(x=>x.classList.toggle('active',x===btn));$('#modalTitle').textContent=formType==='income'?'เพิ่มรายรับ':formType==='transfer'?'โอนเงิน':'เพิ่มรายจ่าย';$('#toWalletLabel').hidden=formType!=='transfer'}));
$('#transactionForm').addEventListener('submit',e=>{e.preventDefault();const data=Object.fromEntries(new FormData(e.target));const amount=Number(data.amount);const wallet=getWallet(data.walletId);if(!wallet||amount<=0)return;const item={id:uid('t'),title:data.title,amount,date:data.date,walletId:data.walletId,toWalletId:data.toWalletId,category:formType==='transfer'?'โอนเงิน':data.category,note:data.note,type:formType};if(formType==='income')wallet.balance+=amount;else if(formType==='expense')wallet.balance-=amount;else{const target=getWallet(data.toWalletId);if(!target||data.walletId===data.toWalletId)return toast('เลือกกระเป๋าปลายทางให้ถูกต้อง');wallet.balance-=amount;target.balance+=amount}state.transactions.push(item);save();closeModal();render();toast('บันทึกรายการเรียบร้อยแล้ว')});
$('#walletForm').addEventListener('submit',e=>{e.preventDefault();const data=Object.fromEntries(new FormData(e.target));state.wallets.push({id:uid('wallet'),name:data.name,icon:data.icon,balance:Number(data.balance)||0,initialBalance:Number(data.balance)||0});save();closeModal('walletModal');e.target.reset();render();toast('สร้างกระเป๋าใหม่แล้ว')});$('#budgetForm').addEventListener('submit',e=>{e.preventDefault();const data=Object.fromEntries(new FormData(e.target));state.budgets.push({id:uid('budget'),name:data.name,amount:Number(data.amount),category:data.category,spent:0});save();closeModal('budgetModal');e.target.reset();render();toast('เพิ่มค่าใช้จ่ายรายเดือนแล้ว')});$('#addWalletButton').addEventListener('click',()=>$('#walletModal').hidden=false);$('#addBudgetButton').addEventListener('click',()=>$('#budgetModal').hidden=false);$('#searchInput').addEventListener('input',renderTransactions);$('#typeFilter').addEventListener('change',renderTransactions);$('#walletFilter').addEventListener('change',renderTransactions);$('#prevMonth').addEventListener('click',()=>{selectedMonth.setMonth(selectedMonth.getMonth()-1);render()});$('#nextMonth').addEventListener('click',()=>{selectedMonth.setMonth(selectedMonth.getMonth()+1);render()});$('#themeToggle').addEventListener('click',()=>document.body.classList.toggle('dark-mode'));$('#exportButton').addEventListener('click',()=>{const blob=new Blob([JSON.stringify(state,null,2)],{type:'application/json'}),a=document.createElement('a');a.href=URL.createObjectURL(blob);a.download=`mymoneyflow-${selectedKey()}.json`;a.click();URL.revokeObjectURL(a.href);toast('ส่งออกข้อมูลแล้ว')});$('#mobileMenu').addEventListener('click',()=>$('.sidebar').classList.toggle('open'));document.addEventListener('keydown',e=>{if((e.metaKey||e.ctrlKey)&&['1','2','3','4'].includes(e.key)){e.preventDefault();navigate(['overview','transactions','wallets','budgets'][Number(e.key)-1])}if(!e.metaKey&&!e.ctrlKey&&!e.altKey&&e.key.toLowerCase()==='n')openTransaction('income');if(!e.metaKey&&!e.ctrlKey&&!e.altKey&&e.key.toLowerCase()==='e')openTransaction('expense');if(e.key==='Escape')$$('.modal-backdrop').forEach(x=>closeModal(x.id))});
render();