Laden...
Verlauf 6 Monate
Darlehen Übersicht
Budget-Ziele
Ratenkäufe
Invest Übersicht
Letzte Buchungen
Budget-Ziele
INVESTIERT
AKTUELL
GESAMT P&L
Neue Buchung
Betrag (€)
Kategorie
Notiz (optional)
Datum
Einträge
CSV einfügen (Datum;Typ;Kategorie;Betrag;Notiz)
LAUFENDER SALDO € 0,00
Darlehen
Ratenkäufe
+real.toFixed(2)+''; if (pnl !== 0) html += '  ·  PnL: function setMvCurrency(cur) { mvCurrency = cur; var eurBtn = document.getElementById('iv-mv-eur'); var usdBtn = document.getElementById('iv-mv-usd'); var hint = document.getElementById('iv-mv-hint'); if (cur === 'EUR') { eurBtn.style.background = 'var(--blue)'; eurBtn.style.color = '#fff'; eurBtn.style.border = 'none'; usdBtn.style.background = 'var(--card2)'; usdBtn.style.color = '#aaa'; usdBtn.style.border = '1px solid var(--border)'; if (hint) hint.style.display = 'none'; } else { usdBtn.style.background = 'var(--blue)'; usdBtn.style.color = '#fff'; usdBtn.style.border = 'none'; eurBtn.style.background = 'var(--card2)'; eurBtn.style.color = '#aaa'; eurBtn.style.border = '1px solid var(--border)'; if (hint) { hint.style.display = 'block'; hint.textContent = '⏳ USD/EUR Kurs wird beim Speichern automatisch geladen…'; } } } async function saveInvestPos() { var name = document.getElementById('iv-name').value.trim(); var menge = parseFloat(document.getElementById('iv-menge').value.toString().replace(',','.')); var kaufpreis = getKaufpreisInEur(); if (!name || isNaN(menge) || isNaN(kaufpreis)) { alert('Bitte Name, Menge und Kaufpreis eingeben'); return; } // Handle USD -> EUR conversion for manual_val var rawMv = parseFloat(document.getElementById('iv-manual-val').value)||0; var manualVal = rawMv; if (mvCurrency === 'USD' && rawMv > 0) { var hint = document.getElementById('iv-mv-hint'); if (hint) hint.textContent = '⏳ USD/EUR Kurs wird geladen…'; var rate = usdEurRate || await fetchUsdEurRate(); manualVal = Math.round(rawMv * rate * 100) / 100; if (hint) hint.textContent = rawMv+' USD × '+rate.toFixed(4)+' = '+manualVal.toFixed(2)+' EUR ✓'; } var row = { name: name, typ: ivTyp, ticker: document.getElementById('iv-ticker').value.trim().toLowerCase(), depot: document.getElementById('iv-depot').value.trim(), menge: menge, kaufpreis: kaufpreis, manual_val: manualVal, notiz: document.getElementById('iv-notiz').value.trim() }; sync('Speichere…'); if (ivEditId) { await db.from('invest').update(row).eq('id', ivEditId); var i = investPositions.findIndex(function(p){ return p.id===ivEditId; }); if (i>=0) investPositions[i] = Object.assign({}, investPositions[i], row); } else { var res = await db.from('invest').insert([row]).select(); if (res.data) investPositions.unshift(res.data[0]); } closeInvestForm(); renderInvest(); fetchLivePrices(); unsync(); } async function deleteInvestPos(id) { if (!confirm('Position löschen?')) return; sync('Lösche…'); await db.from('invest').delete().eq('id', id); investPositions = investPositions.filter(function(p){ return p.id!==id; }); renderInvest(); renderDashInvest(); unsync(); } async function fetchLivePrices() { var cryptoTickers = []; var stockTickers = []; investPositions.forEach(function(p) { if ((p.typ==='crypto') && p.ticker) cryptoTickers.push(p.ticker); if ((p.typ==='stock'||p.typ==='etf') && p.ticker) stockTickers.push(p.ticker); }); // Fetch Crypto via CoinGecko with proxy fallbacks if (cryptoTickers.length > 0) { var ids = [...new Set(cryptoTickers)].join(','); var cgUrl = 'https://api.coingecko.com/api/v3/simple/price?ids='+ids+'&vs_currencies=eur'; var cgProxies = [ cgUrl, // direkt zuerst versuchen 'https://api.allorigins.win/raw?url='+encodeURIComponent(cgUrl), 'https://corsproxy.io/?'+encodeURIComponent(cgUrl), 'https://api.codetabs.com/v1/proxy?quest='+encodeURIComponent(cgUrl) ]; var cryptoFetched = false; for (var ci=0; ci 0) { Object.keys(priceData).forEach(function(k) { if (priceData[k] && priceData[k].eur) livePrices[k] = priceData[k].eur; }); cryptoFetched = true; console.log('Crypto fetched via proxy '+ci); } } catch(e) { console.log('Crypto proxy '+ci+' failed:', e.message); } } if (!cryptoFetched) console.log('All crypto proxies failed'); } // Fetch Stocks via Yahoo Finance with multiple proxies var uniqueStocks = [...new Set(stockTickers)]; for (var i=0; i 0) { if (currency === 'USD') price = price * 0.92; livePrices[sym.toLowerCase()] = price; fetched = true; console.log('Stock '+sym+' fetched: '+price+' via proxy'+pi+' url'+ui); } } catch(e) { console.log('Stock proxy'+pi+'/url'+ui+' failed for '+sym+':', e.message); } } } if (!fetched) console.log('All stock proxies failed for '+sym); } lastPriceUpdate = new Date(); renderInvest(); renderDashInvest(); } function getPrice(pos) { if (pos.typ === 'manual') return pos.manual_val || 0; var key = (pos.ticker || '').toLowerCase(); if (key && livePrices[key] !== undefined) return livePrices[key]; return null; // unknown } function renderInvest() { var depots = ['all']; investPositions.forEach(function(p) { if (p.depot && depots.indexOf(p.depot) === -1) depots.push(p.depot); }); // Depot filter buttons var filterEl = document.getElementById('iv-depot-filter'); if (filterEl) { filterEl.innerHTML = depots.map(function(d) { var active = d === ivDepotFilter; return ''; }).join(''); } var filtered = ivDepotFilter==='all' ? investPositions : investPositions.filter(function(p){ return p.depot===ivDepotFilter; }); var totalInvested = 0, totalCurrent = 0, unknownCount = 0; filtered.forEach(function(p) { var invested = (parseFloat(p.menge)||0) * (parseFloat(p.kaufpreis)||0); totalInvested += invested; var price = getPrice(p); if (price !== null) { totalCurrent += (parseFloat(p.menge)||0) * price; } else { unknownCount++; } }); var pnl = totalCurrent - totalInvested; var pnlPct = totalInvested > 0 ? (pnl/totalInvested*100) : 0; var pnlColor = pnl >= 0 ? 'var(--green)' : 'var(--red)'; var ivInv = document.getElementById('iv-invested'); var ivCur = document.getElementById('iv-current'); var ivPnl = document.getElementById('iv-pnl'); var ivPct = document.getElementById('iv-pnl-pct'); if (ivInv) ivInv.textContent = fmt(totalInvested); if (ivCur) ivCur.textContent = unknownCount>0 ? fmt(totalCurrent)+' *' : fmt(totalCurrent); if (ivPnl) { ivPnl.textContent = (pnl>=0?'+':'')+fmt(pnl); ivPnl.style.color=pnlColor; } if (ivPct) { ivPct.textContent = (pnlPct>=0?'+':'')+pnlPct.toFixed(2)+'%'; ivPct.style.color=pnlColor; } var listEl = document.getElementById('iv-list'); if (!listEl) return; if (filtered.length === 0) { listEl.innerHTML = '
Noch keine Positionen. Füge deine erste Position hinzu!
'; } else { var typIcons = {crypto:'₿', stock:'📊', etf:'📦', manual:'✍️'}; var typLabels = {crypto:'Crypto', stock:'Aktie', etf:'ETF', manual:'Manuell'}; listEl.innerHTML = filtered.map(function(p) { var invested = (parseFloat(p.menge)||0) * (parseFloat(p.kaufpreis)||0); var price = getPrice(p); var currentVal = price !== null ? (parseFloat(p.menge)||0) * price : null; var pnlPos = currentVal !== null ? currentVal - invested : null; var pnlPctPos = invested > 0 && pnlPos !== null ? (pnlPos/invested*100) : null; var pnlColor2 = pnlPos !== null && pnlPos >= 0 ? 'var(--green)' : 'var(--red)'; var priceStr = price !== null ? fmt(price) : 'wird geladen…'; var pnlStr = pnlPos !== null ? ''+(pnlPos>=0?'+':'')+fmt(pnlPos)+' '+(pnlPctPos>=0?'+':'')+pnlPctPos.toFixed(2)+'%' : ''; var html = ''; html += '
'; html += '
'; html += '
'; html += '
'+p.name+'
'; html += '
'+(typIcons[p.typ]||'')+' '+typLabels[p.typ]+(p.depot?' · '+p.depot:'')+(p.ticker?' · '+p.ticker.toUpperCase():'')+'
'; html += '
'; html += '
'; html += ''; html += ''; html += '
'; html += '
'; html += '
Menge
'+parseFloat(p.menge)+'
'; html += '
Ø Kauf
'+fmt(parseFloat(p.kaufpreis))+'
'; html += '
Kurs live
'+priceStr+'
'; html += '
'; html += '
'; // Row 1: Investiert vs Aktuell var currentVal = price !== null ? price * parseFloat(p.menge) : (p.typ === 'manual' ? parseFloat(p.manual_val||0) : null); html += '
'; html += '
Investiert: '+fmt(invested)+'
'; if (currentVal !== null) { html += '
Aktuell: '+fmt(currentVal)+'
'; } else { html += '
Aktuell: wird geladen…
'; } html += '
'; // Row 2: P&L html += '
'+pnlStr+'
'; html += '
'; // Notiz-Zeile html += '
'; html += '
'+(p.notiz ? p.notiz : 'Notiz hinzufügen…')+'
'; html += ''; html += '
'; // Manuelle Positionen: Quick-Wert-Update mit USD/EUR Toggle if (p.typ === "manual") { html += '
'; html += '
'; html += 'Akt. Wert:'; html += ''; html += ''; html += ''; html += '
'; html += ''; html += '
'; } html += '
'; return html; }).join(''); } var infoEl = document.getElementById('iv-update-info'); if (infoEl) { if (lastPriceUpdate) { var msg = 'Kurse: '+lastPriceUpdate.toLocaleTimeString('de-DE'); if (unknownCount > 0) msg += ' · '+unknownCount+' manuell'; infoEl.textContent = msg; infoEl.style.color = 'var(--green)'; } else { infoEl.textContent = 'Kurse konnten nicht geladen werden'; infoEl.style.color = 'var(--red)'; } } } async function ivRefreshPrices() { var btn = document.getElementById('iv-refresh-btn'); var info = document.getElementById('iv-update-info'); if (btn) { btn.textContent = '⏳ Laden…'; btn.disabled = true; } if (info) info.textContent = 'Kurse werden geladen…'; await fetchLivePrices(); if (btn) { btn.textContent = '↻ Aktualisieren'; btn.disabled = false; } } function setIvDepot(d) { ivDepotFilter = d; renderInvest(); } // Event delegation for invest list edit/delete buttons document.addEventListener('click', function(e) { var editId = e.target.getAttribute('data-edit-id'); var delId = e.target.getAttribute('data-del-id'); var qvId = e.target.getAttribute('data-qv-id'); var notizId = e.target.getAttribute('data-notiz-id'); var bzEdit = e.target.getAttribute('data-bz-edit'); var bzDel = e.target.getAttribute('data-bz-del'); if (editId) { openInvestForm(parseInt(editId)); } if (delId) { deleteInvestPos(parseInt(delId)); } if (qvId) { saveQuickVal(parseInt(qvId)); } if (notizId) { openNotizEdit(parseInt(notizId)); } if (bzEdit) { showBudgetForm(bzEdit); } if (bzDel) { deleteBudgetZiel(bzDel); } var qvCurId = e.target.getAttribute('data-qv-cur-id'); if (qvCurId) { toggleQvCurrency(qvCurId); } }); // USD/EUR state per position var qvCurrency = {}; // id -> 'EUR' or 'USD' var usdEurRate = null; async function fetchUsdEurRate() { try { var urls = [ 'https://api.allorigins.win/raw?url='+encodeURIComponent('https://api.frankfurter.app/latest?from=USD&to=EUR'), 'https://corsproxy.io/?'+encodeURIComponent('https://api.frankfurter.app/latest?from=USD&to=EUR'), 'https://api.frankfurter.app/latest?from=USD&to=EUR' ]; for (var i=0; i -1 && s.indexOf('.') > -1) { // Both: assume de-DE format: 2.855,46 s = s.replace(/\./g,'').replace(',','.'); } else { s = s.replace(',','.'); } return parseFloat(s) || 0; } // ── KATEGORIE MODAL ────────────────────────────────────── function openCatModal(type) { const f = filtered().filter(e => e.type === type); const cats = type === 'in' ? CATS_IN : CATS_OUT; const label = type === 'in' ? 'Einnahmen' : 'Ausgaben'; const total = f.reduce((s,e) => s + parseFloat(e.amount), 0); // Group by category const map = {}; f.forEach(e => { if (!map[e.category]) map[e.category] = []; map[e.category].push(e); }); // Sort categories by total descending const sorted = Object.entries(map).sort((a,b) => { const sa = a[1].reduce((s,e)=>s+parseFloat(e.amount),0); const sb = b[1].reduce((s,e)=>s+parseFloat(e.amount),0); return sb - sa; }); document.getElementById('cat-modal-title').textContent = label + ' · ' + MONTHS[curMonth] + ' ' + curYear; document.getElementById('cat-modal-sub').textContent = sorted.length + ' Kategorien · Gesamt: ' + fmt(total); let html = ''; if (!sorted.length) { html = '
Keine Einträge diesen Monat
'; } else { sorted.forEach(([cat, items]) => { const catTotal = items.reduce((s,e)=>s+parseFloat(e.amount),0); const pct = total > 0 ? (catTotal/total*100).toFixed(1) : 0; const color = cats.find(c=>c.l===cat)?.c || '#aaa'; const sortedItems = [...items].sort((a,b)=>new Date(b.date)-new Date(a.date)); html += `
${cat} ${pct}%
${type==='in'?'+':'-'}${fmt(catTotal)}
${sortedItems.map(e=>`
${e.note || '–'}
${e.date}
${fmt(parseFloat(e.amount))}
`).join('')}
`; }); } document.getElementById('cat-modal-body').innerHTML = html; const overlay = document.getElementById('cat-overlay'); overlay.style.display = 'flex'; } function closeCatModal(evt) { if (!evt || evt.target === document.getElementById('cat-overlay')) { document.getElementById('cat-overlay').style.display = 'none'; } } // ── BUDGET ZIELE ───────────────────────────────────────── var budgetZiele = []; var budgetEditId = null; async function loadBudgetZiele() { var res = await db.from('budgetziele').select('*').order('created_at', {ascending:true}); if (res.data) budgetZiele = res.data; renderBudgetZiele(); renderDashBudget(); } function showBudgetForm(id) { budgetEditId = id || null; var cats = CATS_OUT.map(function(c) { return ''; }).join(''); document.getElementById('budget-cat').innerHTML = cats; if (id) { var z = budgetZiele.find(function(x){ return x.id === id; }); if (z) { document.getElementById('budget-cat').value = z.kategorie; document.getElementById('budget-betrag').value = z.betrag; document.getElementById('budget-form-title').textContent = 'Ziel bearbeiten'; } } else { document.getElementById('budget-betrag').value = ''; document.getElementById('budget-form-title').textContent = 'Neues Budget-Ziel'; } document.getElementById('budget-form').style.display = 'block'; document.getElementById('budget-form').scrollIntoView({behavior:'smooth'}); } function hideBudgetForm() { document.getElementById('budget-form').style.display = 'none'; budgetEditId = null; } async function saveBudgetZiel() { var kat = document.getElementById('budget-cat').value; var betrag = parseFloat(document.getElementById('budget-betrag').value.toString().replace(',','.')); if (!kat || !betrag || betrag <= 0) { alert('Bitte Kategorie und Betrag angeben'); return; } var row = { kategorie: kat, betrag: betrag, typ: 'out' }; sync('Speichere…'); if (budgetEditId) { await db.from('budgetziele').update(row).eq('id', budgetEditId); var i = budgetZiele.findIndex(function(x){ return x.id === budgetEditId; }); if (i >= 0) budgetZiele[i] = Object.assign({}, budgetZiele[i], row); } else { var res = await db.from('budgetziele').insert([row]).select(); if (res.data) budgetZiele.push(res.data[0]); } hideBudgetForm(); renderBudgetZiele(); renderDashBudget(); unsync(); } async function deleteBudgetZiel(id) { if (!confirm('Budget-Ziel löschen?')) return; sync('Lösche…'); await db.from('budgetziele').delete().eq('id', id); budgetZiele = budgetZiele.filter(function(x){ return x.id !== id; }); renderBudgetZiele(); renderDashBudget(); unsync(); } function getBudgetSpend() { // Get current month spend per category var f = entries.filter(function(e) { var d = new Date(e.date); return e.type === 'out' && d.getMonth() === curMonth && d.getFullYear() === curYear; }); var spend = {}; f.forEach(function(e) { var cat = e.category || 'Sonstiges'; spend[cat] = (spend[cat] || 0) + parseFloat(e.amount); }); return spend; } function renderBudgetZiele() { var el = document.getElementById('budget-list'); if (!el) return; if (!budgetZiele.length) { el.innerHTML = '
Noch keine Budget-Ziele. Tippe auf "+ Neu" um eines anzulegen.
'; return; } var spend = getBudgetSpend(); var html = ''; budgetZiele.forEach(function(z) { var cat = z.kategorie; var limit = parseFloat(z.betrag); var used = spend[cat] || 0; var pct = Math.min(100, (used / limit * 100)); var over = used > limit; var color = over ? 'var(--red)' : pct > 80 ? '#f59e0b' : 'var(--green)'; var catColor = (CATS_OUT.find(function(c){ return c.l===cat; }) || {}).c || '#aaa'; html += '
'; html += '
'; html += '
'; html += '
'; html += ''+cat+''; html += '
'; html += '
'; html += ''; html += ''; html += '
'; html += '
'; html += 'Verbraucht'; html += ''+used.toLocaleString('de-DE',{minimumFractionDigits:2,maximumFractionDigits:2})+' €'; html += ' / '+limit.toLocaleString('de-DE',{minimumFractionDigits:2,maximumFractionDigits:2})+' €'; html += '
'; html += '
'; html += '
'; html += '
'; html += '
'; if (over) { html += '⚠️ '+(used-limit).toLocaleString('de-DE',{minimumFractionDigits:2,maximumFractionDigits:2})+' € überzogen'; } else { html += ''+(limit-used).toLocaleString('de-DE',{minimumFractionDigits:2,maximumFractionDigits:2})+' € noch verfügbar'; } html += ''+pct.toFixed(0)+'%'; html += '
'; }); el.innerHTML = html; } function renderDashBudget() { var el = document.getElementById('dash-budget-summary'); if (!el) return; if (!budgetZiele.length) { el.innerHTML = '
Noch keine Ziele definiert
'; return; } var spend = getBudgetSpend(); var html = ''; budgetZiele.forEach(function(z) { var cat = z.kategorie; var limit = parseFloat(z.betrag); var used = spend[cat] || 0; var pct = Math.min(100, (used / limit * 100)); var over = used > limit; var color = over ? 'var(--red)' : pct > 80 ? '#f59e0b' : 'var(--green)'; var catColor = (CATS_OUT.find(function(c){ return c.l===cat; }) || {}).c || '#aaa'; html += '
'; html += '
'; html += '
'; html += '
'; html += ''+cat+''; html += '
'; html += ''; html += used.toLocaleString('de-DE',{minimumFractionDigits:2,maximumFractionDigits:2})+' / '+limit.toLocaleString('de-DE',{minimumFractionDigits:2,maximumFractionDigits:2})+' €'; html += '
'; html += '
'; html += '
'; html += '
'; if (over) html += '
⚠️ '+(used-limit).toLocaleString('de-DE',{minimumFractionDigits:2,maximumFractionDigits:2})+' € überzogen
'; html += '
'; }); el.innerHTML = html; } // ── RATENKÄUFE ─────────────────────────────────────────── var ratenkaufe = []; var rkEditId = null; async function loadRatenkaufe() { try { var res = await db.from('ratenkaufe').select('*').order('created_at', {ascending:false}); if (res.data) ratenkaufe = res.data; } catch(e) { try { ratenkaufe = JSON.parse(localStorage.getItem('mb_rk')||'[]'); } catch(e2){ ratenkaufe=[]; } } renderRatenkaufe(); renderDashRatenkaufe(); } function editRatenkauf(id) { var r = ratenkaufe.find(function(x){ return String(x.id) === String(id); }); if (!r) return; rkEditId = id; document.getElementById('rk-form-title').textContent = 'Ratenkauf bearbeiten'; document.getElementById('rk-name').value = r.name || ''; document.getElementById('rk-cat').value = r.kategorie || 'Hardware'; document.getElementById('rk-gesamt').value = parseFloat(r.gesamt).toLocaleString('de-DE', {minimumFractionDigits:2, maximumFractionDigits:2}); document.getElementById('rk-anzahl').value = r.anzahl || ''; document.getElementById('rk-rate').value = parseFloat(r.rate).toLocaleString('de-DE', {minimumFractionDigits:2, maximumFractionDigits:2}); document.getElementById('rk-notiz').value = r.notiz || ''; // Format start date as MM.JJJJ if (r.start_datum) { var d = new Date(r.start_datum); document.getElementById('rk-start').value = String(d.getMonth()+1).padStart(2,'0') + '.' + d.getFullYear(); } calcRkRate(); document.getElementById('rk-form').style.display = 'block'; document.getElementById('rk-form').scrollIntoView({behavior:'smooth'}); } function showRatenkaufForm() { rkEditId = null; document.getElementById('rk-form-title').textContent = 'Neuer Ratenkauf'; document.getElementById('rk-name').value = ''; document.getElementById('rk-gesamt').value = ''; document.getElementById('rk-anzahl').value = ''; document.getElementById('rk-rate').value = ''; document.getElementById('rk-notiz').value = ''; document.getElementById('rk-start').value = '01.' + new Date().getFullYear(); document.getElementById('rk-form').style.display = 'block'; document.getElementById('rk-form').scrollIntoView({behavior:'smooth'}); } function calcRkRate() { var g = parseEuro(document.getElementById('rk-gesamt').value); var n = parseInt(document.getElementById('rk-anzahl').value||0); var startRaw = document.getElementById('rk-start').value.trim(); if (g > 0 && n > 0) { document.getElementById('rk-rate').value = (g/n).toLocaleString('de-DE',{minimumFractionDigits:2,maximumFractionDigits:2}); } // Show letzte Rate preview var letzteRow = document.getElementById('rk-letzte-row'); if (g > 0 && n > 0 && startRaw.length >= 6) { var parts = startRaw.split('.'); var startDate; if (parts.length === 2 && parts[1].length === 4) { startDate = new Date(parseInt(parts[1]), parseInt(parts[0])-1, 1); } else if (parts.length === 3 && parts[2].length === 4) { startDate = new Date(parseInt(parts[2]), parseInt(parts[1])-1, 1); } if (startDate && !isNaN(startDate)) { var endDate = new Date(startDate.getFullYear(), startDate.getMonth() + n - 1, 1); var now = new Date(); var elapsed = Math.max(0, (now.getFullYear()-startDate.getFullYear())*12 + (now.getMonth()-startDate.getMonth())); var bezahltRaten = Math.min(elapsed, n); var bezahltBetrag = bezahltRaten * (g/n); document.getElementById('rk-letzte-label').textContent = endDate.toLocaleDateString('de-DE',{month:'long',year:'numeric'}); document.getElementById('rk-bezahlt-label').textContent = bezahltRaten + ' von ' + n + ' Raten · ' + bezahltBetrag.toLocaleString('de-DE',{minimumFractionDigits:2,maximumFractionDigits:2}) + ' €'; letzteRow.style.display = 'block'; } else { letzteRow.style.display = 'none'; } } else { letzteRow.style.display = 'none'; } } function hideRatenkaufForm() { document.getElementById('rk-form').style.display = 'none'; rkEditId = null; } async function saveRatenkauf() { var name = document.getElementById('rk-name').value.trim(); var gesamt = parseEuro(document.getElementById('rk-gesamt').value); var anzahl = parseInt(document.getElementById('rk-anzahl').value||0); var startRaw = document.getElementById('rk-start').value.trim(); if (!name || !gesamt || !anzahl || !startRaw) { alert('Bitte alle Pflichtfelder ausfüllen'); return; } // Parse MM.JJJJ oder TT.MM.JJJJ var parts = startRaw.split('.'); var startDate; if (parts.length === 2 && parts[1].length === 4) { startDate = new Date(parseInt(parts[1]), parseInt(parts[0])-1, 1); } else if (parts.length === 3) { startDate = new Date(parseInt(parts[2]), parseInt(parts[1])-1, 1); } else { alert('Bitte Startdatum im Format MM.JJJJ eingeben, z.B. 01.2025'); return; } var start = startDate.getFullYear() + '-' + String(startDate.getMonth()+1).padStart(2,'0') + '-01'; var endDate = new Date(startDate.getFullYear(), startDate.getMonth() + anzahl, 1); var endStr = endDate.getFullYear() + '-' + String(endDate.getMonth()+1).padStart(2,'0') + '-01'; var rate = parseEuro(document.getElementById('rk-rate').value) || gesamt/anzahl; var row = { name: name, kategorie: document.getElementById('rk-cat').value, gesamt: gesamt, anzahl: anzahl, rate: rate, start_datum: start, end_datum: endStr, notiz: document.getElementById('rk-notiz').value.trim() }; sync('Speichere…'); try { if (rkEditId) { await db.from('ratenkaufe').update(row).eq('id', rkEditId); var i = ratenkaufe.findIndex(r=>r.id===rkEditId); if (i>=0) ratenkaufe[i] = Object.assign({}, ratenkaufe[i], row); } else { var res = await db.from('ratenkaufe').insert([row]).select(); if (res.data) ratenkaufe.unshift(res.data[0]); else { row.id = Date.now().toString(); ratenkaufe.unshift(row); } } } catch(e) { row.id = Date.now().toString(); ratenkaufe.unshift(row); } localStorage.setItem('mb_rk', JSON.stringify(ratenkaufe)); hideRatenkaufForm(); renderRatenkaufe(); unsync(); } async function deleteRatenkauf(id) { if (!confirm('Ratenkauf löschen?')) return; sync('Lösche…'); try { await db.from('ratenkaufe').delete().eq('id', id); } catch(e){} ratenkaufe = ratenkaufe.filter(r=>String(r.id)!==String(id)); localStorage.setItem('mb_rk', JSON.stringify(ratenkaufe)); renderRatenkaufe(); unsync(); } function renderRatenkaufe() { var now = new Date(); var aktiv = ratenkaufe.filter(r => new Date(r.end_datum) > now); var abgeschlossen = ratenkaufe.filter(r => new Date(r.end_datum) <= now); // Summary var totalMonatlich = aktiv.reduce((s,r)=>s+parseFloat(r.rate||0),0); var summaryEl = document.getElementById('rk-summary'); if (aktiv.length > 0) { summaryEl.innerHTML = `
MONATLICHE BELASTUNG
${fmt(totalMonatlich)}
${aktiv.length} aktive Rate${aktiv.length!==1?'n':''}
`; } else { summaryEl.innerHTML = ''; } var listEl = document.getElementById('rk-list'); if (!ratenkaufe.length) { listEl.innerHTML = '
Noch keine Ratenkäufe erfasst
'; return; } function renderCard(r, isAktiv) { var startDate = new Date(r.start_datum); var endDate = new Date(r.end_datum); var totalMonths = r.anzahl; var nowDate = new Date(); var elapsed = Math.max(0, (nowDate.getFullYear()-startDate.getFullYear())*12 + (nowDate.getMonth()-startDate.getMonth())); var remaining = Math.max(0, totalMonths - elapsed); var pct = Math.min(100, Math.round((elapsed/totalMonths)*100)); var bezahlt = Math.min(elapsed, totalMonths) * parseFloat(r.rate); var rest = parseFloat(r.gesamt) - bezahlt; var endLabel = endDate.toLocaleDateString('de-DE',{month:'long',year:'numeric'}); return `
${r.name}
${r.kategorie}${r.notiz?' · '+r.notiz:''}
${isAktiv?`aktiv`:`fertig`}
Gesamt
${fmt(parseFloat(r.gesamt))}
Rate/Monat
${fmt(parseFloat(r.rate))}
${isAktiv?'Noch':'Ende'}
${isAktiv?remaining+' Monate':endLabel}
Bezahlt: ${fmt(bezahlt)} ${pct}% Ende: ${endLabel}
`; } var html = ''; if (aktiv.length) { html += '
Laufend
'; html += aktiv.map(r=>renderCard(r,true)).join(''); } if (abgeschlossen.length) { html += '
Abgeschlossen
'; html += abgeschlossen.map(r=>renderCard(r,false)).join(''); } listEl.innerHTML = html; } function go(view) { const map={dash:"v-dash",add:"v-add",list:"v-list",darl:"v-darl",invest:"v-invest",raten:"v-raten",budget:"v-budget"}; const nmap={dash:"n-dash",add:"n-add",list:"n-list",darl:"n-darl",invest:"n-invest",raten:"n-raten",budget:"n-budget"}; Object.values(map).forEach(id=>document.getElementById(id).classList.remove("active")); Object.values(nmap).forEach(id=>document.getElementById(id).classList.remove("active")); document.getElementById(map[view]).classList.add("active"); document.getElementById(nmap[view]).classList.add("active"); if (view==="add") { editId=null; document.getElementById("form-title").textContent="Neue Buchung"; document.getElementById("save-btn").textContent="Speichern"; document.getElementById("datum").value=today(); document.getElementById("amount").value=""; document.getElementById("note").value=""; setType("out"); } if (view==="list") renderList(); if (view==="invest") { renderInvest(); fetchLivePrices(); } if (view==="raten") renderRatenkaufe(); if (view==="budget") { renderBudgetZiele(); renderDashBudget(); } } // ── INIT ───────────────────────────────────────────────── buildMonthSelect(); setType("out"); loadEntries(); loadDarlehen(); loadInvest(); loadRatenkaufe(); loadBudgetZiele(); fetchUsdEurRate(); +( pnl>=0?'+':'')+pnl.toFixed(2)+'
'; html += ''; html += '
Echter Wert: function setMvCurrency(cur) { mvCurrency = cur; var eurBtn = document.getElementById('iv-mv-eur'); var usdBtn = document.getElementById('iv-mv-usd'); var hint = document.getElementById('iv-mv-hint'); if (cur === 'EUR') { eurBtn.style.background = 'var(--blue)'; eurBtn.style.color = '#fff'; eurBtn.style.border = 'none'; usdBtn.style.background = 'var(--card2)'; usdBtn.style.color = '#aaa'; usdBtn.style.border = '1px solid var(--border)'; if (hint) hint.style.display = 'none'; } else { usdBtn.style.background = 'var(--blue)'; usdBtn.style.color = '#fff'; usdBtn.style.border = 'none'; eurBtn.style.background = 'var(--card2)'; eurBtn.style.color = '#aaa'; eurBtn.style.border = '1px solid var(--border)'; if (hint) { hint.style.display = 'block'; hint.textContent = '⏳ USD/EUR Kurs wird beim Speichern automatisch geladen…'; } } } async function saveInvestPos() { var name = document.getElementById('iv-name').value.trim(); var menge = parseFloat(document.getElementById('iv-menge').value.toString().replace(',','.')); var kaufpreis = getKaufpreisInEur(); if (!name || isNaN(menge) || isNaN(kaufpreis)) { alert('Bitte Name, Menge und Kaufpreis eingeben'); return; } // Handle USD -> EUR conversion for manual_val var rawMv = parseFloat(document.getElementById('iv-manual-val').value)||0; var manualVal = rawMv; if (mvCurrency === 'USD' && rawMv > 0) { var hint = document.getElementById('iv-mv-hint'); if (hint) hint.textContent = '⏳ USD/EUR Kurs wird geladen…'; var rate = usdEurRate || await fetchUsdEurRate(); manualVal = Math.round(rawMv * rate * 100) / 100; if (hint) hint.textContent = rawMv+' USD × '+rate.toFixed(4)+' = '+manualVal.toFixed(2)+' EUR ✓'; } var row = { name: name, typ: ivTyp, ticker: document.getElementById('iv-ticker').value.trim().toLowerCase(), depot: document.getElementById('iv-depot').value.trim(), menge: menge, kaufpreis: kaufpreis, manual_val: manualVal, notiz: document.getElementById('iv-notiz').value.trim() }; sync('Speichere…'); if (ivEditId) { await db.from('invest').update(row).eq('id', ivEditId); var i = investPositions.findIndex(function(p){ return p.id===ivEditId; }); if (i>=0) investPositions[i] = Object.assign({}, investPositions[i], row); } else { var res = await db.from('invest').insert([row]).select(); if (res.data) investPositions.unshift(res.data[0]); } closeInvestForm(); renderInvest(); fetchLivePrices(); unsync(); } async function deleteInvestPos(id) { if (!confirm('Position löschen?')) return; sync('Lösche…'); await db.from('invest').delete().eq('id', id); investPositions = investPositions.filter(function(p){ return p.id!==id; }); renderInvest(); renderDashInvest(); unsync(); } async function fetchLivePrices() { var cryptoTickers = []; var stockTickers = []; investPositions.forEach(function(p) { if ((p.typ==='crypto') && p.ticker) cryptoTickers.push(p.ticker); if ((p.typ==='stock'||p.typ==='etf') && p.ticker) stockTickers.push(p.ticker); }); // Fetch Crypto via CoinGecko with proxy fallbacks if (cryptoTickers.length > 0) { var ids = [...new Set(cryptoTickers)].join(','); var cgUrl = 'https://api.coingecko.com/api/v3/simple/price?ids='+ids+'&vs_currencies=eur'; var cgProxies = [ cgUrl, // direkt zuerst versuchen 'https://api.allorigins.win/raw?url='+encodeURIComponent(cgUrl), 'https://corsproxy.io/?'+encodeURIComponent(cgUrl), 'https://api.codetabs.com/v1/proxy?quest='+encodeURIComponent(cgUrl) ]; var cryptoFetched = false; for (var ci=0; ci 0) { Object.keys(priceData).forEach(function(k) { if (priceData[k] && priceData[k].eur) livePrices[k] = priceData[k].eur; }); cryptoFetched = true; console.log('Crypto fetched via proxy '+ci); } } catch(e) { console.log('Crypto proxy '+ci+' failed:', e.message); } } if (!cryptoFetched) console.log('All crypto proxies failed'); } // Fetch Stocks via Yahoo Finance with multiple proxies var uniqueStocks = [...new Set(stockTickers)]; for (var i=0; i 0) { if (currency === 'USD') price = price * 0.92; livePrices[sym.toLowerCase()] = price; fetched = true; console.log('Stock '+sym+' fetched: '+price+' via proxy'+pi+' url'+ui); } } catch(e) { console.log('Stock proxy'+pi+'/url'+ui+' failed for '+sym+':', e.message); } } } if (!fetched) console.log('All stock proxies failed for '+sym); } lastPriceUpdate = new Date(); renderInvest(); renderDashInvest(); } function getPrice(pos) { if (pos.typ === 'manual') return pos.manual_val || 0; var key = (pos.ticker || '').toLowerCase(); if (key && livePrices[key] !== undefined) return livePrices[key]; return null; // unknown } function renderInvest() { var depots = ['all']; investPositions.forEach(function(p) { if (p.depot && depots.indexOf(p.depot) === -1) depots.push(p.depot); }); // Depot filter buttons var filterEl = document.getElementById('iv-depot-filter'); if (filterEl) { filterEl.innerHTML = depots.map(function(d) { var active = d === ivDepotFilter; return ''; }).join(''); } var filtered = ivDepotFilter==='all' ? investPositions : investPositions.filter(function(p){ return p.depot===ivDepotFilter; }); var totalInvested = 0, totalCurrent = 0, unknownCount = 0; filtered.forEach(function(p) { var invested = (parseFloat(p.menge)||0) * (parseFloat(p.kaufpreis)||0); totalInvested += invested; var price = getPrice(p); if (price !== null) { totalCurrent += (parseFloat(p.menge)||0) * price; } else { unknownCount++; } }); var pnl = totalCurrent - totalInvested; var pnlPct = totalInvested > 0 ? (pnl/totalInvested*100) : 0; var pnlColor = pnl >= 0 ? 'var(--green)' : 'var(--red)'; var ivInv = document.getElementById('iv-invested'); var ivCur = document.getElementById('iv-current'); var ivPnl = document.getElementById('iv-pnl'); var ivPct = document.getElementById('iv-pnl-pct'); if (ivInv) ivInv.textContent = fmt(totalInvested); if (ivCur) ivCur.textContent = unknownCount>0 ? fmt(totalCurrent)+' *' : fmt(totalCurrent); if (ivPnl) { ivPnl.textContent = (pnl>=0?'+':'')+fmt(pnl); ivPnl.style.color=pnlColor; } if (ivPct) { ivPct.textContent = (pnlPct>=0?'+':'')+pnlPct.toFixed(2)+'%'; ivPct.style.color=pnlColor; } var listEl = document.getElementById('iv-list'); if (!listEl) return; if (filtered.length === 0) { listEl.innerHTML = '
Noch keine Positionen. Füge deine erste Position hinzu!
'; } else { var typIcons = {crypto:'₿', stock:'📊', etf:'📦', manual:'✍️'}; var typLabels = {crypto:'Crypto', stock:'Aktie', etf:'ETF', manual:'Manuell'}; listEl.innerHTML = filtered.map(function(p) { var invested = (parseFloat(p.menge)||0) * (parseFloat(p.kaufpreis)||0); var price = getPrice(p); var currentVal = price !== null ? (parseFloat(p.menge)||0) * price : null; var pnlPos = currentVal !== null ? currentVal - invested : null; var pnlPctPos = invested > 0 && pnlPos !== null ? (pnlPos/invested*100) : null; var pnlColor2 = pnlPos !== null && pnlPos >= 0 ? 'var(--green)' : 'var(--red)'; var priceStr = price !== null ? fmt(price) : 'wird geladen…'; var pnlStr = pnlPos !== null ? ''+(pnlPos>=0?'+':'')+fmt(pnlPos)+' '+(pnlPctPos>=0?'+':'')+pnlPctPos.toFixed(2)+'%' : ''; var html = ''; html += '
'; html += '
'; html += '
'; html += '
'+p.name+'
'; html += '
'+(typIcons[p.typ]||'')+' '+typLabels[p.typ]+(p.depot?' · '+p.depot:'')+(p.ticker?' · '+p.ticker.toUpperCase():'')+'
'; html += '
'; html += '
'; html += ''; html += ''; html += '
'; html += '
'; html += '
Menge
'+parseFloat(p.menge)+'
'; html += '
Ø Kauf
'+fmt(parseFloat(p.kaufpreis))+'
'; html += '
Kurs live
'+priceStr+'
'; html += '
'; html += '
'; // Row 1: Investiert vs Aktuell var currentVal = price !== null ? price * parseFloat(p.menge) : (p.typ === 'manual' ? parseFloat(p.manual_val||0) : null); html += '
'; html += '
Investiert: '+fmt(invested)+'
'; if (currentVal !== null) { html += '
Aktuell: '+fmt(currentVal)+'
'; } else { html += '
Aktuell: wird geladen…
'; } html += '
'; // Row 2: P&L html += '
'+pnlStr+'
'; html += '
'; // Notiz-Zeile html += '
'; html += '
'+(p.notiz ? p.notiz : 'Notiz hinzufügen…')+'
'; html += ''; html += '
'; // Manuelle Positionen: Quick-Wert-Update mit USD/EUR Toggle if (p.typ === "manual") { html += '
'; html += '
'; html += 'Akt. Wert:'; html += ''; html += ''; html += ''; html += '
'; html += ''; html += '
'; } html += '
'; return html; }).join(''); } var infoEl = document.getElementById('iv-update-info'); if (infoEl) { if (lastPriceUpdate) { var msg = 'Kurse: '+lastPriceUpdate.toLocaleTimeString('de-DE'); if (unknownCount > 0) msg += ' · '+unknownCount+' manuell'; infoEl.textContent = msg; infoEl.style.color = 'var(--green)'; } else { infoEl.textContent = 'Kurse konnten nicht geladen werden'; infoEl.style.color = 'var(--red)'; } } } async function ivRefreshPrices() { var btn = document.getElementById('iv-refresh-btn'); var info = document.getElementById('iv-update-info'); if (btn) { btn.textContent = '⏳ Laden…'; btn.disabled = true; } if (info) info.textContent = 'Kurse werden geladen…'; await fetchLivePrices(); if (btn) { btn.textContent = '↻ Aktualisieren'; btn.disabled = false; } } function setIvDepot(d) { ivDepotFilter = d; renderInvest(); } // Event delegation for invest list edit/delete buttons document.addEventListener('click', function(e) { var editId = e.target.getAttribute('data-edit-id'); var delId = e.target.getAttribute('data-del-id'); var qvId = e.target.getAttribute('data-qv-id'); var notizId = e.target.getAttribute('data-notiz-id'); var bzEdit = e.target.getAttribute('data-bz-edit'); var bzDel = e.target.getAttribute('data-bz-del'); if (editId) { openInvestForm(parseInt(editId)); } if (delId) { deleteInvestPos(parseInt(delId)); } if (qvId) { saveQuickVal(parseInt(qvId)); } if (notizId) { openNotizEdit(parseInt(notizId)); } if (bzEdit) { showBudgetForm(bzEdit); } if (bzDel) { deleteBudgetZiel(bzDel); } var qvCurId = e.target.getAttribute('data-qv-cur-id'); if (qvCurId) { toggleQvCurrency(qvCurId); } }); // USD/EUR state per position var qvCurrency = {}; // id -> 'EUR' or 'USD' var usdEurRate = null; async function fetchUsdEurRate() { try { var urls = [ 'https://api.allorigins.win/raw?url='+encodeURIComponent('https://api.frankfurter.app/latest?from=USD&to=EUR'), 'https://corsproxy.io/?'+encodeURIComponent('https://api.frankfurter.app/latest?from=USD&to=EUR'), 'https://api.frankfurter.app/latest?from=USD&to=EUR' ]; for (var i=0; i -1 && s.indexOf('.') > -1) { // Both: assume de-DE format: 2.855,46 s = s.replace(/\./g,'').replace(',','.'); } else { s = s.replace(',','.'); } return parseFloat(s) || 0; } // ── KATEGORIE MODAL ────────────────────────────────────── function openCatModal(type) { const f = filtered().filter(e => e.type === type); const cats = type === 'in' ? CATS_IN : CATS_OUT; const label = type === 'in' ? 'Einnahmen' : 'Ausgaben'; const total = f.reduce((s,e) => s + parseFloat(e.amount), 0); // Group by category const map = {}; f.forEach(e => { if (!map[e.category]) map[e.category] = []; map[e.category].push(e); }); // Sort categories by total descending const sorted = Object.entries(map).sort((a,b) => { const sa = a[1].reduce((s,e)=>s+parseFloat(e.amount),0); const sb = b[1].reduce((s,e)=>s+parseFloat(e.amount),0); return sb - sa; }); document.getElementById('cat-modal-title').textContent = label + ' · ' + MONTHS[curMonth] + ' ' + curYear; document.getElementById('cat-modal-sub').textContent = sorted.length + ' Kategorien · Gesamt: ' + fmt(total); let html = ''; if (!sorted.length) { html = '
Keine Einträge diesen Monat
'; } else { sorted.forEach(([cat, items]) => { const catTotal = items.reduce((s,e)=>s+parseFloat(e.amount),0); const pct = total > 0 ? (catTotal/total*100).toFixed(1) : 0; const color = cats.find(c=>c.l===cat)?.c || '#aaa'; const sortedItems = [...items].sort((a,b)=>new Date(b.date)-new Date(a.date)); html += `
${cat} ${pct}%
${type==='in'?'+':'-'}${fmt(catTotal)}
${sortedItems.map(e=>`
${e.note || '–'}
${e.date}
${fmt(parseFloat(e.amount))}
`).join('')}
`; }); } document.getElementById('cat-modal-body').innerHTML = html; const overlay = document.getElementById('cat-overlay'); overlay.style.display = 'flex'; } function closeCatModal(evt) { if (!evt || evt.target === document.getElementById('cat-overlay')) { document.getElementById('cat-overlay').style.display = 'none'; } } // ── BUDGET ZIELE ───────────────────────────────────────── var budgetZiele = []; var budgetEditId = null; async function loadBudgetZiele() { var res = await db.from('budgetziele').select('*').order('created_at', {ascending:true}); if (res.data) budgetZiele = res.data; renderBudgetZiele(); renderDashBudget(); } function showBudgetForm(id) { budgetEditId = id || null; var cats = CATS_OUT.map(function(c) { return ''; }).join(''); document.getElementById('budget-cat').innerHTML = cats; if (id) { var z = budgetZiele.find(function(x){ return x.id === id; }); if (z) { document.getElementById('budget-cat').value = z.kategorie; document.getElementById('budget-betrag').value = z.betrag; document.getElementById('budget-form-title').textContent = 'Ziel bearbeiten'; } } else { document.getElementById('budget-betrag').value = ''; document.getElementById('budget-form-title').textContent = 'Neues Budget-Ziel'; } document.getElementById('budget-form').style.display = 'block'; document.getElementById('budget-form').scrollIntoView({behavior:'smooth'}); } function hideBudgetForm() { document.getElementById('budget-form').style.display = 'none'; budgetEditId = null; } async function saveBudgetZiel() { var kat = document.getElementById('budget-cat').value; var betrag = parseFloat(document.getElementById('budget-betrag').value.toString().replace(',','.')); if (!kat || !betrag || betrag <= 0) { alert('Bitte Kategorie und Betrag angeben'); return; } var row = { kategorie: kat, betrag: betrag, typ: 'out' }; sync('Speichere…'); if (budgetEditId) { await db.from('budgetziele').update(row).eq('id', budgetEditId); var i = budgetZiele.findIndex(function(x){ return x.id === budgetEditId; }); if (i >= 0) budgetZiele[i] = Object.assign({}, budgetZiele[i], row); } else { var res = await db.from('budgetziele').insert([row]).select(); if (res.data) budgetZiele.push(res.data[0]); } hideBudgetForm(); renderBudgetZiele(); renderDashBudget(); unsync(); } async function deleteBudgetZiel(id) { if (!confirm('Budget-Ziel löschen?')) return; sync('Lösche…'); await db.from('budgetziele').delete().eq('id', id); budgetZiele = budgetZiele.filter(function(x){ return x.id !== id; }); renderBudgetZiele(); renderDashBudget(); unsync(); } function getBudgetSpend() { // Get current month spend per category var f = entries.filter(function(e) { var d = new Date(e.date); return e.type === 'out' && d.getMonth() === curMonth && d.getFullYear() === curYear; }); var spend = {}; f.forEach(function(e) { var cat = e.category || 'Sonstiges'; spend[cat] = (spend[cat] || 0) + parseFloat(e.amount); }); return spend; } function renderBudgetZiele() { var el = document.getElementById('budget-list'); if (!el) return; if (!budgetZiele.length) { el.innerHTML = '
Noch keine Budget-Ziele. Tippe auf "+ Neu" um eines anzulegen.
'; return; } var spend = getBudgetSpend(); var html = ''; budgetZiele.forEach(function(z) { var cat = z.kategorie; var limit = parseFloat(z.betrag); var used = spend[cat] || 0; var pct = Math.min(100, (used / limit * 100)); var over = used > limit; var color = over ? 'var(--red)' : pct > 80 ? '#f59e0b' : 'var(--green)'; var catColor = (CATS_OUT.find(function(c){ return c.l===cat; }) || {}).c || '#aaa'; html += '
'; html += '
'; html += '
'; html += '
'; html += ''+cat+''; html += '
'; html += '
'; html += ''; html += ''; html += '
'; html += '
'; html += 'Verbraucht'; html += ''+used.toLocaleString('de-DE',{minimumFractionDigits:2,maximumFractionDigits:2})+' €'; html += ' / '+limit.toLocaleString('de-DE',{minimumFractionDigits:2,maximumFractionDigits:2})+' €'; html += '
'; html += '
'; html += '
'; html += '
'; html += '
'; if (over) { html += '⚠️ '+(used-limit).toLocaleString('de-DE',{minimumFractionDigits:2,maximumFractionDigits:2})+' € überzogen'; } else { html += ''+(limit-used).toLocaleString('de-DE',{minimumFractionDigits:2,maximumFractionDigits:2})+' € noch verfügbar'; } html += ''+pct.toFixed(0)+'%'; html += '
'; }); el.innerHTML = html; } function renderDashBudget() { var el = document.getElementById('dash-budget-summary'); if (!el) return; if (!budgetZiele.length) { el.innerHTML = '
Noch keine Ziele definiert
'; return; } var spend = getBudgetSpend(); var html = ''; budgetZiele.forEach(function(z) { var cat = z.kategorie; var limit = parseFloat(z.betrag); var used = spend[cat] || 0; var pct = Math.min(100, (used / limit * 100)); var over = used > limit; var color = over ? 'var(--red)' : pct > 80 ? '#f59e0b' : 'var(--green)'; var catColor = (CATS_OUT.find(function(c){ return c.l===cat; }) || {}).c || '#aaa'; html += '
'; html += '
'; html += '
'; html += '
'; html += ''+cat+''; html += '
'; html += ''; html += used.toLocaleString('de-DE',{minimumFractionDigits:2,maximumFractionDigits:2})+' / '+limit.toLocaleString('de-DE',{minimumFractionDigits:2,maximumFractionDigits:2})+' €'; html += '
'; html += '
'; html += '
'; html += '
'; if (over) html += '
⚠️ '+(used-limit).toLocaleString('de-DE',{minimumFractionDigits:2,maximumFractionDigits:2})+' € überzogen
'; html += '
'; }); el.innerHTML = html; } // ── RATENKÄUFE ─────────────────────────────────────────── var ratenkaufe = []; var rkEditId = null; async function loadRatenkaufe() { try { var res = await db.from('ratenkaufe').select('*').order('created_at', {ascending:false}); if (res.data) ratenkaufe = res.data; } catch(e) { try { ratenkaufe = JSON.parse(localStorage.getItem('mb_rk')||'[]'); } catch(e2){ ratenkaufe=[]; } } renderRatenkaufe(); renderDashRatenkaufe(); } function editRatenkauf(id) { var r = ratenkaufe.find(function(x){ return String(x.id) === String(id); }); if (!r) return; rkEditId = id; document.getElementById('rk-form-title').textContent = 'Ratenkauf bearbeiten'; document.getElementById('rk-name').value = r.name || ''; document.getElementById('rk-cat').value = r.kategorie || 'Hardware'; document.getElementById('rk-gesamt').value = parseFloat(r.gesamt).toLocaleString('de-DE', {minimumFractionDigits:2, maximumFractionDigits:2}); document.getElementById('rk-anzahl').value = r.anzahl || ''; document.getElementById('rk-rate').value = parseFloat(r.rate).toLocaleString('de-DE', {minimumFractionDigits:2, maximumFractionDigits:2}); document.getElementById('rk-notiz').value = r.notiz || ''; // Format start date as MM.JJJJ if (r.start_datum) { var d = new Date(r.start_datum); document.getElementById('rk-start').value = String(d.getMonth()+1).padStart(2,'0') + '.' + d.getFullYear(); } calcRkRate(); document.getElementById('rk-form').style.display = 'block'; document.getElementById('rk-form').scrollIntoView({behavior:'smooth'}); } function showRatenkaufForm() { rkEditId = null; document.getElementById('rk-form-title').textContent = 'Neuer Ratenkauf'; document.getElementById('rk-name').value = ''; document.getElementById('rk-gesamt').value = ''; document.getElementById('rk-anzahl').value = ''; document.getElementById('rk-rate').value = ''; document.getElementById('rk-notiz').value = ''; document.getElementById('rk-start').value = '01.' + new Date().getFullYear(); document.getElementById('rk-form').style.display = 'block'; document.getElementById('rk-form').scrollIntoView({behavior:'smooth'}); } function calcRkRate() { var g = parseEuro(document.getElementById('rk-gesamt').value); var n = parseInt(document.getElementById('rk-anzahl').value||0); var startRaw = document.getElementById('rk-start').value.trim(); if (g > 0 && n > 0) { document.getElementById('rk-rate').value = (g/n).toLocaleString('de-DE',{minimumFractionDigits:2,maximumFractionDigits:2}); } // Show letzte Rate preview var letzteRow = document.getElementById('rk-letzte-row'); if (g > 0 && n > 0 && startRaw.length >= 6) { var parts = startRaw.split('.'); var startDate; if (parts.length === 2 && parts[1].length === 4) { startDate = new Date(parseInt(parts[1]), parseInt(parts[0])-1, 1); } else if (parts.length === 3 && parts[2].length === 4) { startDate = new Date(parseInt(parts[2]), parseInt(parts[1])-1, 1); } if (startDate && !isNaN(startDate)) { var endDate = new Date(startDate.getFullYear(), startDate.getMonth() + n - 1, 1); var now = new Date(); var elapsed = Math.max(0, (now.getFullYear()-startDate.getFullYear())*12 + (now.getMonth()-startDate.getMonth())); var bezahltRaten = Math.min(elapsed, n); var bezahltBetrag = bezahltRaten * (g/n); document.getElementById('rk-letzte-label').textContent = endDate.toLocaleDateString('de-DE',{month:'long',year:'numeric'}); document.getElementById('rk-bezahlt-label').textContent = bezahltRaten + ' von ' + n + ' Raten · ' + bezahltBetrag.toLocaleString('de-DE',{minimumFractionDigits:2,maximumFractionDigits:2}) + ' €'; letzteRow.style.display = 'block'; } else { letzteRow.style.display = 'none'; } } else { letzteRow.style.display = 'none'; } } function hideRatenkaufForm() { document.getElementById('rk-form').style.display = 'none'; rkEditId = null; } async function saveRatenkauf() { var name = document.getElementById('rk-name').value.trim(); var gesamt = parseEuro(document.getElementById('rk-gesamt').value); var anzahl = parseInt(document.getElementById('rk-anzahl').value||0); var startRaw = document.getElementById('rk-start').value.trim(); if (!name || !gesamt || !anzahl || !startRaw) { alert('Bitte alle Pflichtfelder ausfüllen'); return; } // Parse MM.JJJJ oder TT.MM.JJJJ var parts = startRaw.split('.'); var startDate; if (parts.length === 2 && parts[1].length === 4) { startDate = new Date(parseInt(parts[1]), parseInt(parts[0])-1, 1); } else if (parts.length === 3) { startDate = new Date(parseInt(parts[2]), parseInt(parts[1])-1, 1); } else { alert('Bitte Startdatum im Format MM.JJJJ eingeben, z.B. 01.2025'); return; } var start = startDate.getFullYear() + '-' + String(startDate.getMonth()+1).padStart(2,'0') + '-01'; var endDate = new Date(startDate.getFullYear(), startDate.getMonth() + anzahl, 1); var endStr = endDate.getFullYear() + '-' + String(endDate.getMonth()+1).padStart(2,'0') + '-01'; var rate = parseEuro(document.getElementById('rk-rate').value) || gesamt/anzahl; var row = { name: name, kategorie: document.getElementById('rk-cat').value, gesamt: gesamt, anzahl: anzahl, rate: rate, start_datum: start, end_datum: endStr, notiz: document.getElementById('rk-notiz').value.trim() }; sync('Speichere…'); try { if (rkEditId) { await db.from('ratenkaufe').update(row).eq('id', rkEditId); var i = ratenkaufe.findIndex(r=>r.id===rkEditId); if (i>=0) ratenkaufe[i] = Object.assign({}, ratenkaufe[i], row); } else { var res = await db.from('ratenkaufe').insert([row]).select(); if (res.data) ratenkaufe.unshift(res.data[0]); else { row.id = Date.now().toString(); ratenkaufe.unshift(row); } } } catch(e) { row.id = Date.now().toString(); ratenkaufe.unshift(row); } localStorage.setItem('mb_rk', JSON.stringify(ratenkaufe)); hideRatenkaufForm(); renderRatenkaufe(); unsync(); } async function deleteRatenkauf(id) { if (!confirm('Ratenkauf löschen?')) return; sync('Lösche…'); try { await db.from('ratenkaufe').delete().eq('id', id); } catch(e){} ratenkaufe = ratenkaufe.filter(r=>String(r.id)!==String(id)); localStorage.setItem('mb_rk', JSON.stringify(ratenkaufe)); renderRatenkaufe(); unsync(); } function renderRatenkaufe() { var now = new Date(); var aktiv = ratenkaufe.filter(r => new Date(r.end_datum) > now); var abgeschlossen = ratenkaufe.filter(r => new Date(r.end_datum) <= now); // Summary var totalMonatlich = aktiv.reduce((s,r)=>s+parseFloat(r.rate||0),0); var summaryEl = document.getElementById('rk-summary'); if (aktiv.length > 0) { summaryEl.innerHTML = `
MONATLICHE BELASTUNG
${fmt(totalMonatlich)}
${aktiv.length} aktive Rate${aktiv.length!==1?'n':''}
`; } else { summaryEl.innerHTML = ''; } var listEl = document.getElementById('rk-list'); if (!ratenkaufe.length) { listEl.innerHTML = '
Noch keine Ratenkäufe erfasst
'; return; } function renderCard(r, isAktiv) { var startDate = new Date(r.start_datum); var endDate = new Date(r.end_datum); var totalMonths = r.anzahl; var nowDate = new Date(); var elapsed = Math.max(0, (nowDate.getFullYear()-startDate.getFullYear())*12 + (nowDate.getMonth()-startDate.getMonth())); var remaining = Math.max(0, totalMonths - elapsed); var pct = Math.min(100, Math.round((elapsed/totalMonths)*100)); var bezahlt = Math.min(elapsed, totalMonths) * parseFloat(r.rate); var rest = parseFloat(r.gesamt) - bezahlt; var endLabel = endDate.toLocaleDateString('de-DE',{month:'long',year:'numeric'}); return `
${r.name}
${r.kategorie}${r.notiz?' · '+r.notiz:''}
${isAktiv?`aktiv`:`fertig`}
Gesamt
${fmt(parseFloat(r.gesamt))}
Rate/Monat
${fmt(parseFloat(r.rate))}
${isAktiv?'Noch':'Ende'}
${isAktiv?remaining+' Monate':endLabel}
Bezahlt: ${fmt(bezahlt)} ${pct}% Ende: ${endLabel}
`; } var html = ''; if (aktiv.length) { html += '
Laufend
'; html += aktiv.map(r=>renderCard(r,true)).join(''); } if (abgeschlossen.length) { html += '
Abgeschlossen
'; html += abgeschlossen.map(r=>renderCard(r,false)).join(''); } listEl.innerHTML = html; } function go(view) { const map={dash:"v-dash",add:"v-add",list:"v-list",darl:"v-darl",invest:"v-invest",raten:"v-raten",budget:"v-budget"}; const nmap={dash:"n-dash",add:"n-add",list:"n-list",darl:"n-darl",invest:"n-invest",raten:"n-raten",budget:"n-budget"}; Object.values(map).forEach(id=>document.getElementById(id).classList.remove("active")); Object.values(nmap).forEach(id=>document.getElementById(id).classList.remove("active")); document.getElementById(map[view]).classList.add("active"); document.getElementById(nmap[view]).classList.add("active"); if (view==="add") { editId=null; document.getElementById("form-title").textContent="Neue Buchung"; document.getElementById("save-btn").textContent="Speichern"; document.getElementById("datum").value=today(); document.getElementById("amount").value=""; document.getElementById("note").value=""; setType("out"); } if (view==="list") renderList(); if (view==="invest") { renderInvest(); fetchLivePrices(); } if (view==="raten") renderRatenkaufe(); if (view==="budget") { renderBudgetZiele(); renderDashBudget(); } } // ── INIT ───────────────────────────────────────────────── buildMonthSelect(); setType("out"); loadEntries(); loadDarlehen(); loadInvest(); loadRatenkaufe(); loadBudgetZiele(); fetchUsdEurRate(); +echterWertUsd.toFixed(2)+'
= '+echterWertEur.toFixed(2)+' €
'; html += '
Balance function setMvCurrency(cur) { mvCurrency = cur; var eurBtn = document.getElementById('iv-mv-eur'); var usdBtn = document.getElementById('iv-mv-usd'); var hint = document.getElementById('iv-mv-hint'); if (cur === 'EUR') { eurBtn.style.background = 'var(--blue)'; eurBtn.style.color = '#fff'; eurBtn.style.border = 'none'; usdBtn.style.background = 'var(--card2)'; usdBtn.style.color = '#aaa'; usdBtn.style.border = '1px solid var(--border)'; if (hint) hint.style.display = 'none'; } else { usdBtn.style.background = 'var(--blue)'; usdBtn.style.color = '#fff'; usdBtn.style.border = 'none'; eurBtn.style.background = 'var(--card2)'; eurBtn.style.color = '#aaa'; eurBtn.style.border = '1px solid var(--border)'; if (hint) { hint.style.display = 'block'; hint.textContent = '⏳ USD/EUR Kurs wird beim Speichern automatisch geladen…'; } } } async function saveInvestPos() { var name = document.getElementById('iv-name').value.trim(); var menge = parseFloat(document.getElementById('iv-menge').value.toString().replace(',','.')); var kaufpreis = getKaufpreisInEur(); if (!name || isNaN(menge) || isNaN(kaufpreis)) { alert('Bitte Name, Menge und Kaufpreis eingeben'); return; } // Handle USD -> EUR conversion for manual_val var rawMv = parseFloat(document.getElementById('iv-manual-val').value)||0; var manualVal = rawMv; if (mvCurrency === 'USD' && rawMv > 0) { var hint = document.getElementById('iv-mv-hint'); if (hint) hint.textContent = '⏳ USD/EUR Kurs wird geladen…'; var rate = usdEurRate || await fetchUsdEurRate(); manualVal = Math.round(rawMv * rate * 100) / 100; if (hint) hint.textContent = rawMv+' USD × '+rate.toFixed(4)+' = '+manualVal.toFixed(2)+' EUR ✓'; } var row = { name: name, typ: ivTyp, ticker: document.getElementById('iv-ticker').value.trim().toLowerCase(), depot: document.getElementById('iv-depot').value.trim(), menge: menge, kaufpreis: kaufpreis, manual_val: manualVal, notiz: document.getElementById('iv-notiz').value.trim() }; sync('Speichere…'); if (ivEditId) { await db.from('invest').update(row).eq('id', ivEditId); var i = investPositions.findIndex(function(p){ return p.id===ivEditId; }); if (i>=0) investPositions[i] = Object.assign({}, investPositions[i], row); } else { var res = await db.from('invest').insert([row]).select(); if (res.data) investPositions.unshift(res.data[0]); } closeInvestForm(); renderInvest(); fetchLivePrices(); unsync(); } async function deleteInvestPos(id) { if (!confirm('Position löschen?')) return; sync('Lösche…'); await db.from('invest').delete().eq('id', id); investPositions = investPositions.filter(function(p){ return p.id!==id; }); renderInvest(); renderDashInvest(); unsync(); } async function fetchLivePrices() { var cryptoTickers = []; var stockTickers = []; investPositions.forEach(function(p) { if ((p.typ==='crypto') && p.ticker) cryptoTickers.push(p.ticker); if ((p.typ==='stock'||p.typ==='etf') && p.ticker) stockTickers.push(p.ticker); }); // Fetch Crypto via CoinGecko with proxy fallbacks if (cryptoTickers.length > 0) { var ids = [...new Set(cryptoTickers)].join(','); var cgUrl = 'https://api.coingecko.com/api/v3/simple/price?ids='+ids+'&vs_currencies=eur'; var cgProxies = [ cgUrl, // direkt zuerst versuchen 'https://api.allorigins.win/raw?url='+encodeURIComponent(cgUrl), 'https://corsproxy.io/?'+encodeURIComponent(cgUrl), 'https://api.codetabs.com/v1/proxy?quest='+encodeURIComponent(cgUrl) ]; var cryptoFetched = false; for (var ci=0; ci 0) { Object.keys(priceData).forEach(function(k) { if (priceData[k] && priceData[k].eur) livePrices[k] = priceData[k].eur; }); cryptoFetched = true; console.log('Crypto fetched via proxy '+ci); } } catch(e) { console.log('Crypto proxy '+ci+' failed:', e.message); } } if (!cryptoFetched) console.log('All crypto proxies failed'); } // Fetch Stocks via Yahoo Finance with multiple proxies var uniqueStocks = [...new Set(stockTickers)]; for (var i=0; i 0) { if (currency === 'USD') price = price * 0.92; livePrices[sym.toLowerCase()] = price; fetched = true; console.log('Stock '+sym+' fetched: '+price+' via proxy'+pi+' url'+ui); } } catch(e) { console.log('Stock proxy'+pi+'/url'+ui+' failed for '+sym+':', e.message); } } } if (!fetched) console.log('All stock proxies failed for '+sym); } lastPriceUpdate = new Date(); renderInvest(); renderDashInvest(); } function getPrice(pos) { if (pos.typ === 'manual') return pos.manual_val || 0; var key = (pos.ticker || '').toLowerCase(); if (key && livePrices[key] !== undefined) return livePrices[key]; return null; // unknown } function renderInvest() { var depots = ['all']; investPositions.forEach(function(p) { if (p.depot && depots.indexOf(p.depot) === -1) depots.push(p.depot); }); // Depot filter buttons var filterEl = document.getElementById('iv-depot-filter'); if (filterEl) { filterEl.innerHTML = depots.map(function(d) { var active = d === ivDepotFilter; return ''; }).join(''); } var filtered = ivDepotFilter==='all' ? investPositions : investPositions.filter(function(p){ return p.depot===ivDepotFilter; }); var totalInvested = 0, totalCurrent = 0, unknownCount = 0; filtered.forEach(function(p) { var invested = (parseFloat(p.menge)||0) * (parseFloat(p.kaufpreis)||0); totalInvested += invested; var price = getPrice(p); if (price !== null) { totalCurrent += (parseFloat(p.menge)||0) * price; } else { unknownCount++; } }); var pnl = totalCurrent - totalInvested; var pnlPct = totalInvested > 0 ? (pnl/totalInvested*100) : 0; var pnlColor = pnl >= 0 ? 'var(--green)' : 'var(--red)'; var ivInv = document.getElementById('iv-invested'); var ivCur = document.getElementById('iv-current'); var ivPnl = document.getElementById('iv-pnl'); var ivPct = document.getElementById('iv-pnl-pct'); if (ivInv) ivInv.textContent = fmt(totalInvested); if (ivCur) ivCur.textContent = unknownCount>0 ? fmt(totalCurrent)+' *' : fmt(totalCurrent); if (ivPnl) { ivPnl.textContent = (pnl>=0?'+':'')+fmt(pnl); ivPnl.style.color=pnlColor; } if (ivPct) { ivPct.textContent = (pnlPct>=0?'+':'')+pnlPct.toFixed(2)+'%'; ivPct.style.color=pnlColor; } var listEl = document.getElementById('iv-list'); if (!listEl) return; if (filtered.length === 0) { listEl.innerHTML = '
Noch keine Positionen. Füge deine erste Position hinzu!
'; } else { var typIcons = {crypto:'₿', stock:'📊', etf:'📦', manual:'✍️'}; var typLabels = {crypto:'Crypto', stock:'Aktie', etf:'ETF', manual:'Manuell'}; listEl.innerHTML = filtered.map(function(p) { var invested = (parseFloat(p.menge)||0) * (parseFloat(p.kaufpreis)||0); var price = getPrice(p); var currentVal = price !== null ? (parseFloat(p.menge)||0) * price : null; var pnlPos = currentVal !== null ? currentVal - invested : null; var pnlPctPos = invested > 0 && pnlPos !== null ? (pnlPos/invested*100) : null; var pnlColor2 = pnlPos !== null && pnlPos >= 0 ? 'var(--green)' : 'var(--red)'; var priceStr = price !== null ? fmt(price) : 'wird geladen…'; var pnlStr = pnlPos !== null ? ''+(pnlPos>=0?'+':'')+fmt(pnlPos)+' '+(pnlPctPos>=0?'+':'')+pnlPctPos.toFixed(2)+'%' : ''; var html = ''; html += '
'; html += '
'; html += '
'; html += '
'+p.name+'
'; html += '
'+(typIcons[p.typ]||'')+' '+typLabels[p.typ]+(p.depot?' · '+p.depot:'')+(p.ticker?' · '+p.ticker.toUpperCase():'')+'
'; html += '
'; html += '
'; html += ''; html += ''; html += '
'; html += '
'; html += '
Menge
'+parseFloat(p.menge)+'
'; html += '
Ø Kauf
'+fmt(parseFloat(p.kaufpreis))+'
'; html += '
Kurs live
'+priceStr+'
'; html += '
'; html += '
'; // Row 1: Investiert vs Aktuell var currentVal = price !== null ? price * parseFloat(p.menge) : (p.typ === 'manual' ? parseFloat(p.manual_val||0) : null); html += '
'; html += '
Investiert: '+fmt(invested)+'
'; if (currentVal !== null) { html += '
Aktuell: '+fmt(currentVal)+'
'; } else { html += '
Aktuell: wird geladen…
'; } html += '
'; // Row 2: P&L html += '
'+pnlStr+'
'; html += '
'; // Notiz-Zeile html += '
'; html += '
'+(p.notiz ? p.notiz : 'Notiz hinzufügen…')+'
'; html += ''; html += '
'; // Manuelle Positionen: Quick-Wert-Update mit USD/EUR Toggle if (p.typ === "manual") { html += '
'; html += '
'; html += 'Akt. Wert:'; html += ''; html += ''; html += ''; html += '
'; html += ''; html += '
'; } html += '
'; return html; }).join(''); } var infoEl = document.getElementById('iv-update-info'); if (infoEl) { if (lastPriceUpdate) { var msg = 'Kurse: '+lastPriceUpdate.toLocaleTimeString('de-DE'); if (unknownCount > 0) msg += ' · '+unknownCount+' manuell'; infoEl.textContent = msg; infoEl.style.color = 'var(--green)'; } else { infoEl.textContent = 'Kurse konnten nicht geladen werden'; infoEl.style.color = 'var(--red)'; } } } async function ivRefreshPrices() { var btn = document.getElementById('iv-refresh-btn'); var info = document.getElementById('iv-update-info'); if (btn) { btn.textContent = '⏳ Laden…'; btn.disabled = true; } if (info) info.textContent = 'Kurse werden geladen…'; await fetchLivePrices(); if (btn) { btn.textContent = '↻ Aktualisieren'; btn.disabled = false; } } function setIvDepot(d) { ivDepotFilter = d; renderInvest(); } // Event delegation for invest list edit/delete buttons document.addEventListener('click', function(e) { var editId = e.target.getAttribute('data-edit-id'); var delId = e.target.getAttribute('data-del-id'); var qvId = e.target.getAttribute('data-qv-id'); var notizId = e.target.getAttribute('data-notiz-id'); var bzEdit = e.target.getAttribute('data-bz-edit'); var bzDel = e.target.getAttribute('data-bz-del'); if (editId) { openInvestForm(parseInt(editId)); } if (delId) { deleteInvestPos(parseInt(delId)); } if (qvId) { saveQuickVal(parseInt(qvId)); } if (notizId) { openNotizEdit(parseInt(notizId)); } if (bzEdit) { showBudgetForm(bzEdit); } if (bzDel) { deleteBudgetZiel(bzDel); } var qvCurId = e.target.getAttribute('data-qv-cur-id'); if (qvCurId) { toggleQvCurrency(qvCurId); } }); // USD/EUR state per position var qvCurrency = {}; // id -> 'EUR' or 'USD' var usdEurRate = null; async function fetchUsdEurRate() { try { var urls = [ 'https://api.allorigins.win/raw?url='+encodeURIComponent('https://api.frankfurter.app/latest?from=USD&to=EUR'), 'https://corsproxy.io/?'+encodeURIComponent('https://api.frankfurter.app/latest?from=USD&to=EUR'), 'https://api.frankfurter.app/latest?from=USD&to=EUR' ]; for (var i=0; i -1 && s.indexOf('.') > -1) { // Both: assume de-DE format: 2.855,46 s = s.replace(/\./g,'').replace(',','.'); } else { s = s.replace(',','.'); } return parseFloat(s) || 0; } // ── KATEGORIE MODAL ────────────────────────────────────── function openCatModal(type) { const f = filtered().filter(e => e.type === type); const cats = type === 'in' ? CATS_IN : CATS_OUT; const label = type === 'in' ? 'Einnahmen' : 'Ausgaben'; const total = f.reduce((s,e) => s + parseFloat(e.amount), 0); // Group by category const map = {}; f.forEach(e => { if (!map[e.category]) map[e.category] = []; map[e.category].push(e); }); // Sort categories by total descending const sorted = Object.entries(map).sort((a,b) => { const sa = a[1].reduce((s,e)=>s+parseFloat(e.amount),0); const sb = b[1].reduce((s,e)=>s+parseFloat(e.amount),0); return sb - sa; }); document.getElementById('cat-modal-title').textContent = label + ' · ' + MONTHS[curMonth] + ' ' + curYear; document.getElementById('cat-modal-sub').textContent = sorted.length + ' Kategorien · Gesamt: ' + fmt(total); let html = ''; if (!sorted.length) { html = '
Keine Einträge diesen Monat
'; } else { sorted.forEach(([cat, items]) => { const catTotal = items.reduce((s,e)=>s+parseFloat(e.amount),0); const pct = total > 0 ? (catTotal/total*100).toFixed(1) : 0; const color = cats.find(c=>c.l===cat)?.c || '#aaa'; const sortedItems = [...items].sort((a,b)=>new Date(b.date)-new Date(a.date)); html += `
${cat} ${pct}%
${type==='in'?'+':'-'}${fmt(catTotal)}
${sortedItems.map(e=>`
${e.note || '–'}
${e.date}
${fmt(parseFloat(e.amount))}
`).join('')}
`; }); } document.getElementById('cat-modal-body').innerHTML = html; const overlay = document.getElementById('cat-overlay'); overlay.style.display = 'flex'; } function closeCatModal(evt) { if (!evt || evt.target === document.getElementById('cat-overlay')) { document.getElementById('cat-overlay').style.display = 'none'; } } // ── BUDGET ZIELE ───────────────────────────────────────── var budgetZiele = []; var budgetEditId = null; async function loadBudgetZiele() { var res = await db.from('budgetziele').select('*').order('created_at', {ascending:true}); if (res.data) budgetZiele = res.data; renderBudgetZiele(); renderDashBudget(); } function showBudgetForm(id) { budgetEditId = id || null; var cats = CATS_OUT.map(function(c) { return ''; }).join(''); document.getElementById('budget-cat').innerHTML = cats; if (id) { var z = budgetZiele.find(function(x){ return x.id === id; }); if (z) { document.getElementById('budget-cat').value = z.kategorie; document.getElementById('budget-betrag').value = z.betrag; document.getElementById('budget-form-title').textContent = 'Ziel bearbeiten'; } } else { document.getElementById('budget-betrag').value = ''; document.getElementById('budget-form-title').textContent = 'Neues Budget-Ziel'; } document.getElementById('budget-form').style.display = 'block'; document.getElementById('budget-form').scrollIntoView({behavior:'smooth'}); } function hideBudgetForm() { document.getElementById('budget-form').style.display = 'none'; budgetEditId = null; } async function saveBudgetZiel() { var kat = document.getElementById('budget-cat').value; var betrag = parseFloat(document.getElementById('budget-betrag').value.toString().replace(',','.')); if (!kat || !betrag || betrag <= 0) { alert('Bitte Kategorie und Betrag angeben'); return; } var row = { kategorie: kat, betrag: betrag, typ: 'out' }; sync('Speichere…'); if (budgetEditId) { await db.from('budgetziele').update(row).eq('id', budgetEditId); var i = budgetZiele.findIndex(function(x){ return x.id === budgetEditId; }); if (i >= 0) budgetZiele[i] = Object.assign({}, budgetZiele[i], row); } else { var res = await db.from('budgetziele').insert([row]).select(); if (res.data) budgetZiele.push(res.data[0]); } hideBudgetForm(); renderBudgetZiele(); renderDashBudget(); unsync(); } async function deleteBudgetZiel(id) { if (!confirm('Budget-Ziel löschen?')) return; sync('Lösche…'); await db.from('budgetziele').delete().eq('id', id); budgetZiele = budgetZiele.filter(function(x){ return x.id !== id; }); renderBudgetZiele(); renderDashBudget(); unsync(); } function getBudgetSpend() { // Get current month spend per category var f = entries.filter(function(e) { var d = new Date(e.date); return e.type === 'out' && d.getMonth() === curMonth && d.getFullYear() === curYear; }); var spend = {}; f.forEach(function(e) { var cat = e.category || 'Sonstiges'; spend[cat] = (spend[cat] || 0) + parseFloat(e.amount); }); return spend; } function renderBudgetZiele() { var el = document.getElementById('budget-list'); if (!el) return; if (!budgetZiele.length) { el.innerHTML = '
Noch keine Budget-Ziele. Tippe auf "+ Neu" um eines anzulegen.
'; return; } var spend = getBudgetSpend(); var html = ''; budgetZiele.forEach(function(z) { var cat = z.kategorie; var limit = parseFloat(z.betrag); var used = spend[cat] || 0; var pct = Math.min(100, (used / limit * 100)); var over = used > limit; var color = over ? 'var(--red)' : pct > 80 ? '#f59e0b' : 'var(--green)'; var catColor = (CATS_OUT.find(function(c){ return c.l===cat; }) || {}).c || '#aaa'; html += '
'; html += '
'; html += '
'; html += '
'; html += ''+cat+''; html += '
'; html += '
'; html += ''; html += ''; html += '
'; html += '
'; html += 'Verbraucht'; html += ''+used.toLocaleString('de-DE',{minimumFractionDigits:2,maximumFractionDigits:2})+' €'; html += ' / '+limit.toLocaleString('de-DE',{minimumFractionDigits:2,maximumFractionDigits:2})+' €'; html += '
'; html += '
'; html += '
'; html += '
'; html += '
'; if (over) { html += '⚠️ '+(used-limit).toLocaleString('de-DE',{minimumFractionDigits:2,maximumFractionDigits:2})+' € überzogen'; } else { html += ''+(limit-used).toLocaleString('de-DE',{minimumFractionDigits:2,maximumFractionDigits:2})+' € noch verfügbar'; } html += ''+pct.toFixed(0)+'%'; html += '
'; }); el.innerHTML = html; } function renderDashBudget() { var el = document.getElementById('dash-budget-summary'); if (!el) return; if (!budgetZiele.length) { el.innerHTML = '
Noch keine Ziele definiert
'; return; } var spend = getBudgetSpend(); var html = ''; budgetZiele.forEach(function(z) { var cat = z.kategorie; var limit = parseFloat(z.betrag); var used = spend[cat] || 0; var pct = Math.min(100, (used / limit * 100)); var over = used > limit; var color = over ? 'var(--red)' : pct > 80 ? '#f59e0b' : 'var(--green)'; var catColor = (CATS_OUT.find(function(c){ return c.l===cat; }) || {}).c || '#aaa'; html += '
'; html += '
'; html += '
'; html += '
'; html += ''+cat+''; html += '
'; html += ''; html += used.toLocaleString('de-DE',{minimumFractionDigits:2,maximumFractionDigits:2})+' / '+limit.toLocaleString('de-DE',{minimumFractionDigits:2,maximumFractionDigits:2})+' €'; html += '
'; html += '
'; html += '
'; html += '
'; if (over) html += '
⚠️ '+(used-limit).toLocaleString('de-DE',{minimumFractionDigits:2,maximumFractionDigits:2})+' € überzogen
'; html += '
'; }); el.innerHTML = html; } // ── RATENKÄUFE ─────────────────────────────────────────── var ratenkaufe = []; var rkEditId = null; async function loadRatenkaufe() { try { var res = await db.from('ratenkaufe').select('*').order('created_at', {ascending:false}); if (res.data) ratenkaufe = res.data; } catch(e) { try { ratenkaufe = JSON.parse(localStorage.getItem('mb_rk')||'[]'); } catch(e2){ ratenkaufe=[]; } } renderRatenkaufe(); renderDashRatenkaufe(); } function editRatenkauf(id) { var r = ratenkaufe.find(function(x){ return String(x.id) === String(id); }); if (!r) return; rkEditId = id; document.getElementById('rk-form-title').textContent = 'Ratenkauf bearbeiten'; document.getElementById('rk-name').value = r.name || ''; document.getElementById('rk-cat').value = r.kategorie || 'Hardware'; document.getElementById('rk-gesamt').value = parseFloat(r.gesamt).toLocaleString('de-DE', {minimumFractionDigits:2, maximumFractionDigits:2}); document.getElementById('rk-anzahl').value = r.anzahl || ''; document.getElementById('rk-rate').value = parseFloat(r.rate).toLocaleString('de-DE', {minimumFractionDigits:2, maximumFractionDigits:2}); document.getElementById('rk-notiz').value = r.notiz || ''; // Format start date as MM.JJJJ if (r.start_datum) { var d = new Date(r.start_datum); document.getElementById('rk-start').value = String(d.getMonth()+1).padStart(2,'0') + '.' + d.getFullYear(); } calcRkRate(); document.getElementById('rk-form').style.display = 'block'; document.getElementById('rk-form').scrollIntoView({behavior:'smooth'}); } function showRatenkaufForm() { rkEditId = null; document.getElementById('rk-form-title').textContent = 'Neuer Ratenkauf'; document.getElementById('rk-name').value = ''; document.getElementById('rk-gesamt').value = ''; document.getElementById('rk-anzahl').value = ''; document.getElementById('rk-rate').value = ''; document.getElementById('rk-notiz').value = ''; document.getElementById('rk-start').value = '01.' + new Date().getFullYear(); document.getElementById('rk-form').style.display = 'block'; document.getElementById('rk-form').scrollIntoView({behavior:'smooth'}); } function calcRkRate() { var g = parseEuro(document.getElementById('rk-gesamt').value); var n = parseInt(document.getElementById('rk-anzahl').value||0); var startRaw = document.getElementById('rk-start').value.trim(); if (g > 0 && n > 0) { document.getElementById('rk-rate').value = (g/n).toLocaleString('de-DE',{minimumFractionDigits:2,maximumFractionDigits:2}); } // Show letzte Rate preview var letzteRow = document.getElementById('rk-letzte-row'); if (g > 0 && n > 0 && startRaw.length >= 6) { var parts = startRaw.split('.'); var startDate; if (parts.length === 2 && parts[1].length === 4) { startDate = new Date(parseInt(parts[1]), parseInt(parts[0])-1, 1); } else if (parts.length === 3 && parts[2].length === 4) { startDate = new Date(parseInt(parts[2]), parseInt(parts[1])-1, 1); } if (startDate && !isNaN(startDate)) { var endDate = new Date(startDate.getFullYear(), startDate.getMonth() + n - 1, 1); var now = new Date(); var elapsed = Math.max(0, (now.getFullYear()-startDate.getFullYear())*12 + (now.getMonth()-startDate.getMonth())); var bezahltRaten = Math.min(elapsed, n); var bezahltBetrag = bezahltRaten * (g/n); document.getElementById('rk-letzte-label').textContent = endDate.toLocaleDateString('de-DE',{month:'long',year:'numeric'}); document.getElementById('rk-bezahlt-label').textContent = bezahltRaten + ' von ' + n + ' Raten · ' + bezahltBetrag.toLocaleString('de-DE',{minimumFractionDigits:2,maximumFractionDigits:2}) + ' €'; letzteRow.style.display = 'block'; } else { letzteRow.style.display = 'none'; } } else { letzteRow.style.display = 'none'; } } function hideRatenkaufForm() { document.getElementById('rk-form').style.display = 'none'; rkEditId = null; } async function saveRatenkauf() { var name = document.getElementById('rk-name').value.trim(); var gesamt = parseEuro(document.getElementById('rk-gesamt').value); var anzahl = parseInt(document.getElementById('rk-anzahl').value||0); var startRaw = document.getElementById('rk-start').value.trim(); if (!name || !gesamt || !anzahl || !startRaw) { alert('Bitte alle Pflichtfelder ausfüllen'); return; } // Parse MM.JJJJ oder TT.MM.JJJJ var parts = startRaw.split('.'); var startDate; if (parts.length === 2 && parts[1].length === 4) { startDate = new Date(parseInt(parts[1]), parseInt(parts[0])-1, 1); } else if (parts.length === 3) { startDate = new Date(parseInt(parts[2]), parseInt(parts[1])-1, 1); } else { alert('Bitte Startdatum im Format MM.JJJJ eingeben, z.B. 01.2025'); return; } var start = startDate.getFullYear() + '-' + String(startDate.getMonth()+1).padStart(2,'0') + '-01'; var endDate = new Date(startDate.getFullYear(), startDate.getMonth() + anzahl, 1); var endStr = endDate.getFullYear() + '-' + String(endDate.getMonth()+1).padStart(2,'0') + '-01'; var rate = parseEuro(document.getElementById('rk-rate').value) || gesamt/anzahl; var row = { name: name, kategorie: document.getElementById('rk-cat').value, gesamt: gesamt, anzahl: anzahl, rate: rate, start_datum: start, end_datum: endStr, notiz: document.getElementById('rk-notiz').value.trim() }; sync('Speichere…'); try { if (rkEditId) { await db.from('ratenkaufe').update(row).eq('id', rkEditId); var i = ratenkaufe.findIndex(r=>r.id===rkEditId); if (i>=0) ratenkaufe[i] = Object.assign({}, ratenkaufe[i], row); } else { var res = await db.from('ratenkaufe').insert([row]).select(); if (res.data) ratenkaufe.unshift(res.data[0]); else { row.id = Date.now().toString(); ratenkaufe.unshift(row); } } } catch(e) { row.id = Date.now().toString(); ratenkaufe.unshift(row); } localStorage.setItem('mb_rk', JSON.stringify(ratenkaufe)); hideRatenkaufForm(); renderRatenkaufe(); unsync(); } async function deleteRatenkauf(id) { if (!confirm('Ratenkauf löschen?')) return; sync('Lösche…'); try { await db.from('ratenkaufe').delete().eq('id', id); } catch(e){} ratenkaufe = ratenkaufe.filter(r=>String(r.id)!==String(id)); localStorage.setItem('mb_rk', JSON.stringify(ratenkaufe)); renderRatenkaufe(); unsync(); } function renderRatenkaufe() { var now = new Date(); var aktiv = ratenkaufe.filter(r => new Date(r.end_datum) > now); var abgeschlossen = ratenkaufe.filter(r => new Date(r.end_datum) <= now); // Summary var totalMonatlich = aktiv.reduce((s,r)=>s+parseFloat(r.rate||0),0); var summaryEl = document.getElementById('rk-summary'); if (aktiv.length > 0) { summaryEl.innerHTML = `
MONATLICHE BELASTUNG
${fmt(totalMonatlich)}
${aktiv.length} aktive Rate${aktiv.length!==1?'n':''}
`; } else { summaryEl.innerHTML = ''; } var listEl = document.getElementById('rk-list'); if (!ratenkaufe.length) { listEl.innerHTML = '
Noch keine Ratenkäufe erfasst
'; return; } function renderCard(r, isAktiv) { var startDate = new Date(r.start_datum); var endDate = new Date(r.end_datum); var totalMonths = r.anzahl; var nowDate = new Date(); var elapsed = Math.max(0, (nowDate.getFullYear()-startDate.getFullYear())*12 + (nowDate.getMonth()-startDate.getMonth())); var remaining = Math.max(0, totalMonths - elapsed); var pct = Math.min(100, Math.round((elapsed/totalMonths)*100)); var bezahlt = Math.min(elapsed, totalMonths) * parseFloat(r.rate); var rest = parseFloat(r.gesamt) - bezahlt; var endLabel = endDate.toLocaleDateString('de-DE',{month:'long',year:'numeric'}); return `
${r.name}
${r.kategorie}${r.notiz?' · '+r.notiz:''}
${isAktiv?`aktiv`:`fertig`}
Gesamt
${fmt(parseFloat(r.gesamt))}
Rate/Monat
${fmt(parseFloat(r.rate))}
${isAktiv?'Noch':'Ende'}
${isAktiv?remaining+' Monate':endLabel}
Bezahlt: ${fmt(bezahlt)} ${pct}% Ende: ${endLabel}
`; } var html = ''; if (aktiv.length) { html += '
Laufend
'; html += aktiv.map(r=>renderCard(r,true)).join(''); } if (abgeschlossen.length) { html += '
Abgeschlossen
'; html += abgeschlossen.map(r=>renderCard(r,false)).join(''); } listEl.innerHTML = html; } function go(view) { const map={dash:"v-dash",add:"v-add",list:"v-list",darl:"v-darl",invest:"v-invest",raten:"v-raten",budget:"v-budget"}; const nmap={dash:"n-dash",add:"n-add",list:"n-list",darl:"n-darl",invest:"n-invest",raten:"n-raten",budget:"n-budget"}; Object.values(map).forEach(id=>document.getElementById(id).classList.remove("active")); Object.values(nmap).forEach(id=>document.getElementById(id).classList.remove("active")); document.getElementById(map[view]).classList.add("active"); document.getElementById(nmap[view]).classList.add("active"); if (view==="add") { editId=null; document.getElementById("form-title").textContent="Neue Buchung"; document.getElementById("save-btn").textContent="Speichern"; document.getElementById("datum").value=today(); document.getElementById("amount").value=""; document.getElementById("note").value=""; setType("out"); } if (view==="list") renderList(); if (view==="invest") { renderInvest(); fetchLivePrices(); } if (view==="raten") renderRatenkaufe(); if (view==="budget") { renderBudgetZiele(); renderDashBudget(); } } // ── INIT ───────────────────────────────────────────────── buildMonthSelect(); setType("out"); loadEntries(); loadDarlehen(); loadInvest(); loadRatenkaufe(); loadBudgetZiele(); fetchUsdEurRate(); +(balance/hebel).toFixed(2)+' (×'+hebel+' Hebel) → Basis: function setMvCurrency(cur) { mvCurrency = cur; var eurBtn = document.getElementById('iv-mv-eur'); var usdBtn = document.getElementById('iv-mv-usd'); var hint = document.getElementById('iv-mv-hint'); if (cur === 'EUR') { eurBtn.style.background = 'var(--blue)'; eurBtn.style.color = '#fff'; eurBtn.style.border = 'none'; usdBtn.style.background = 'var(--card2)'; usdBtn.style.color = '#aaa'; usdBtn.style.border = '1px solid var(--border)'; if (hint) hint.style.display = 'none'; } else { usdBtn.style.background = 'var(--blue)'; usdBtn.style.color = '#fff'; usdBtn.style.border = 'none'; eurBtn.style.background = 'var(--card2)'; eurBtn.style.color = '#aaa'; eurBtn.style.border = '1px solid var(--border)'; if (hint) { hint.style.display = 'block'; hint.textContent = '⏳ USD/EUR Kurs wird beim Speichern automatisch geladen…'; } } } async function saveInvestPos() { var name = document.getElementById('iv-name').value.trim(); var menge = parseFloat(document.getElementById('iv-menge').value.toString().replace(',','.')); var kaufpreis = getKaufpreisInEur(); if (!name || isNaN(menge) || isNaN(kaufpreis)) { alert('Bitte Name, Menge und Kaufpreis eingeben'); return; } // Handle USD -> EUR conversion for manual_val var rawMv = parseFloat(document.getElementById('iv-manual-val').value)||0; var manualVal = rawMv; if (mvCurrency === 'USD' && rawMv > 0) { var hint = document.getElementById('iv-mv-hint'); if (hint) hint.textContent = '⏳ USD/EUR Kurs wird geladen…'; var rate = usdEurRate || await fetchUsdEurRate(); manualVal = Math.round(rawMv * rate * 100) / 100; if (hint) hint.textContent = rawMv+' USD × '+rate.toFixed(4)+' = '+manualVal.toFixed(2)+' EUR ✓'; } var row = { name: name, typ: ivTyp, ticker: document.getElementById('iv-ticker').value.trim().toLowerCase(), depot: document.getElementById('iv-depot').value.trim(), menge: menge, kaufpreis: kaufpreis, manual_val: manualVal, notiz: document.getElementById('iv-notiz').value.trim() }; sync('Speichere…'); if (ivEditId) { await db.from('invest').update(row).eq('id', ivEditId); var i = investPositions.findIndex(function(p){ return p.id===ivEditId; }); if (i>=0) investPositions[i] = Object.assign({}, investPositions[i], row); } else { var res = await db.from('invest').insert([row]).select(); if (res.data) investPositions.unshift(res.data[0]); } closeInvestForm(); renderInvest(); fetchLivePrices(); unsync(); } async function deleteInvestPos(id) { if (!confirm('Position löschen?')) return; sync('Lösche…'); await db.from('invest').delete().eq('id', id); investPositions = investPositions.filter(function(p){ return p.id!==id; }); renderInvest(); renderDashInvest(); unsync(); } async function fetchLivePrices() { var cryptoTickers = []; var stockTickers = []; investPositions.forEach(function(p) { if ((p.typ==='crypto') && p.ticker) cryptoTickers.push(p.ticker); if ((p.typ==='stock'||p.typ==='etf') && p.ticker) stockTickers.push(p.ticker); }); // Fetch Crypto via CoinGecko with proxy fallbacks if (cryptoTickers.length > 0) { var ids = [...new Set(cryptoTickers)].join(','); var cgUrl = 'https://api.coingecko.com/api/v3/simple/price?ids='+ids+'&vs_currencies=eur'; var cgProxies = [ cgUrl, // direkt zuerst versuchen 'https://api.allorigins.win/raw?url='+encodeURIComponent(cgUrl), 'https://corsproxy.io/?'+encodeURIComponent(cgUrl), 'https://api.codetabs.com/v1/proxy?quest='+encodeURIComponent(cgUrl) ]; var cryptoFetched = false; for (var ci=0; ci 0) { Object.keys(priceData).forEach(function(k) { if (priceData[k] && priceData[k].eur) livePrices[k] = priceData[k].eur; }); cryptoFetched = true; console.log('Crypto fetched via proxy '+ci); } } catch(e) { console.log('Crypto proxy '+ci+' failed:', e.message); } } if (!cryptoFetched) console.log('All crypto proxies failed'); } // Fetch Stocks via Yahoo Finance with multiple proxies var uniqueStocks = [...new Set(stockTickers)]; for (var i=0; i 0) { if (currency === 'USD') price = price * 0.92; livePrices[sym.toLowerCase()] = price; fetched = true; console.log('Stock '+sym+' fetched: '+price+' via proxy'+pi+' url'+ui); } } catch(e) { console.log('Stock proxy'+pi+'/url'+ui+' failed for '+sym+':', e.message); } } } if (!fetched) console.log('All stock proxies failed for '+sym); } lastPriceUpdate = new Date(); renderInvest(); renderDashInvest(); } function getPrice(pos) { if (pos.typ === 'manual') return pos.manual_val || 0; var key = (pos.ticker || '').toLowerCase(); if (key && livePrices[key] !== undefined) return livePrices[key]; return null; // unknown } function renderInvest() { var depots = ['all']; investPositions.forEach(function(p) { if (p.depot && depots.indexOf(p.depot) === -1) depots.push(p.depot); }); // Depot filter buttons var filterEl = document.getElementById('iv-depot-filter'); if (filterEl) { filterEl.innerHTML = depots.map(function(d) { var active = d === ivDepotFilter; return ''; }).join(''); } var filtered = ivDepotFilter==='all' ? investPositions : investPositions.filter(function(p){ return p.depot===ivDepotFilter; }); var totalInvested = 0, totalCurrent = 0, unknownCount = 0; filtered.forEach(function(p) { var invested = (parseFloat(p.menge)||0) * (parseFloat(p.kaufpreis)||0); totalInvested += invested; var price = getPrice(p); if (price !== null) { totalCurrent += (parseFloat(p.menge)||0) * price; } else { unknownCount++; } }); var pnl = totalCurrent - totalInvested; var pnlPct = totalInvested > 0 ? (pnl/totalInvested*100) : 0; var pnlColor = pnl >= 0 ? 'var(--green)' : 'var(--red)'; var ivInv = document.getElementById('iv-invested'); var ivCur = document.getElementById('iv-current'); var ivPnl = document.getElementById('iv-pnl'); var ivPct = document.getElementById('iv-pnl-pct'); if (ivInv) ivInv.textContent = fmt(totalInvested); if (ivCur) ivCur.textContent = unknownCount>0 ? fmt(totalCurrent)+' *' : fmt(totalCurrent); if (ivPnl) { ivPnl.textContent = (pnl>=0?'+':'')+fmt(pnl); ivPnl.style.color=pnlColor; } if (ivPct) { ivPct.textContent = (pnlPct>=0?'+':'')+pnlPct.toFixed(2)+'%'; ivPct.style.color=pnlColor; } var listEl = document.getElementById('iv-list'); if (!listEl) return; if (filtered.length === 0) { listEl.innerHTML = '
Noch keine Positionen. Füge deine erste Position hinzu!
'; } else { var typIcons = {crypto:'₿', stock:'📊', etf:'📦', manual:'✍️'}; var typLabels = {crypto:'Crypto', stock:'Aktie', etf:'ETF', manual:'Manuell'}; listEl.innerHTML = filtered.map(function(p) { var invested = (parseFloat(p.menge)||0) * (parseFloat(p.kaufpreis)||0); var price = getPrice(p); var currentVal = price !== null ? (parseFloat(p.menge)||0) * price : null; var pnlPos = currentVal !== null ? currentVal - invested : null; var pnlPctPos = invested > 0 && pnlPos !== null ? (pnlPos/invested*100) : null; var pnlColor2 = pnlPos !== null && pnlPos >= 0 ? 'var(--green)' : 'var(--red)'; var priceStr = price !== null ? fmt(price) : 'wird geladen…'; var pnlStr = pnlPos !== null ? ''+(pnlPos>=0?'+':'')+fmt(pnlPos)+' '+(pnlPctPos>=0?'+':'')+pnlPctPos.toFixed(2)+'%' : ''; var html = ''; html += '
'; html += '
'; html += '
'; html += '
'+p.name+'
'; html += '
'+(typIcons[p.typ]||'')+' '+typLabels[p.typ]+(p.depot?' · '+p.depot:'')+(p.ticker?' · '+p.ticker.toUpperCase():'')+'
'; html += '
'; html += '
'; html += ''; html += ''; html += '
'; html += '
'; html += '
Menge
'+parseFloat(p.menge)+'
'; html += '
Ø Kauf
'+fmt(parseFloat(p.kaufpreis))+'
'; html += '
Kurs live
'+priceStr+'
'; html += '
'; html += '
'; // Row 1: Investiert vs Aktuell var currentVal = price !== null ? price * parseFloat(p.menge) : (p.typ === 'manual' ? parseFloat(p.manual_val||0) : null); html += '
'; html += '
Investiert: '+fmt(invested)+'
'; if (currentVal !== null) { html += '
Aktuell: '+fmt(currentVal)+'
'; } else { html += '
Aktuell: wird geladen…
'; } html += '
'; // Row 2: P&L html += '
'+pnlStr+'
'; html += '
'; // Notiz-Zeile html += '
'; html += '
'+(p.notiz ? p.notiz : 'Notiz hinzufügen…')+'
'; html += ''; html += '
'; // Manuelle Positionen: Quick-Wert-Update mit USD/EUR Toggle if (p.typ === "manual") { html += '
'; html += '
'; html += 'Akt. Wert:'; html += ''; html += ''; html += ''; html += '
'; html += ''; html += '
'; } html += '
'; return html; }).join(''); } var infoEl = document.getElementById('iv-update-info'); if (infoEl) { if (lastPriceUpdate) { var msg = 'Kurse: '+lastPriceUpdate.toLocaleTimeString('de-DE'); if (unknownCount > 0) msg += ' · '+unknownCount+' manuell'; infoEl.textContent = msg; infoEl.style.color = 'var(--green)'; } else { infoEl.textContent = 'Kurse konnten nicht geladen werden'; infoEl.style.color = 'var(--red)'; } } } async function ivRefreshPrices() { var btn = document.getElementById('iv-refresh-btn'); var info = document.getElementById('iv-update-info'); if (btn) { btn.textContent = '⏳ Laden…'; btn.disabled = true; } if (info) info.textContent = 'Kurse werden geladen…'; await fetchLivePrices(); if (btn) { btn.textContent = '↻ Aktualisieren'; btn.disabled = false; } } function setIvDepot(d) { ivDepotFilter = d; renderInvest(); } // Event delegation for invest list edit/delete buttons document.addEventListener('click', function(e) { var editId = e.target.getAttribute('data-edit-id'); var delId = e.target.getAttribute('data-del-id'); var qvId = e.target.getAttribute('data-qv-id'); var notizId = e.target.getAttribute('data-notiz-id'); var bzEdit = e.target.getAttribute('data-bz-edit'); var bzDel = e.target.getAttribute('data-bz-del'); if (editId) { openInvestForm(parseInt(editId)); } if (delId) { deleteInvestPos(parseInt(delId)); } if (qvId) { saveQuickVal(parseInt(qvId)); } if (notizId) { openNotizEdit(parseInt(notizId)); } if (bzEdit) { showBudgetForm(bzEdit); } if (bzDel) { deleteBudgetZiel(bzDel); } var qvCurId = e.target.getAttribute('data-qv-cur-id'); if (qvCurId) { toggleQvCurrency(qvCurId); } }); // USD/EUR state per position var qvCurrency = {}; // id -> 'EUR' or 'USD' var usdEurRate = null; async function fetchUsdEurRate() { try { var urls = [ 'https://api.allorigins.win/raw?url='+encodeURIComponent('https://api.frankfurter.app/latest?from=USD&to=EUR'), 'https://corsproxy.io/?'+encodeURIComponent('https://api.frankfurter.app/latest?from=USD&to=EUR'), 'https://api.frankfurter.app/latest?from=USD&to=EUR' ]; for (var i=0; i -1 && s.indexOf('.') > -1) { // Both: assume de-DE format: 2.855,46 s = s.replace(/\./g,'').replace(',','.'); } else { s = s.replace(',','.'); } return parseFloat(s) || 0; } // ── KATEGORIE MODAL ────────────────────────────────────── function openCatModal(type) { const f = filtered().filter(e => e.type === type); const cats = type === 'in' ? CATS_IN : CATS_OUT; const label = type === 'in' ? 'Einnahmen' : 'Ausgaben'; const total = f.reduce((s,e) => s + parseFloat(e.amount), 0); // Group by category const map = {}; f.forEach(e => { if (!map[e.category]) map[e.category] = []; map[e.category].push(e); }); // Sort categories by total descending const sorted = Object.entries(map).sort((a,b) => { const sa = a[1].reduce((s,e)=>s+parseFloat(e.amount),0); const sb = b[1].reduce((s,e)=>s+parseFloat(e.amount),0); return sb - sa; }); document.getElementById('cat-modal-title').textContent = label + ' · ' + MONTHS[curMonth] + ' ' + curYear; document.getElementById('cat-modal-sub').textContent = sorted.length + ' Kategorien · Gesamt: ' + fmt(total); let html = ''; if (!sorted.length) { html = '
Keine Einträge diesen Monat
'; } else { sorted.forEach(([cat, items]) => { const catTotal = items.reduce((s,e)=>s+parseFloat(e.amount),0); const pct = total > 0 ? (catTotal/total*100).toFixed(1) : 0; const color = cats.find(c=>c.l===cat)?.c || '#aaa'; const sortedItems = [...items].sort((a,b)=>new Date(b.date)-new Date(a.date)); html += `
${cat} ${pct}%
${type==='in'?'+':'-'}${fmt(catTotal)}
${sortedItems.map(e=>`
${e.note || '–'}
${e.date}
${fmt(parseFloat(e.amount))}
`).join('')}
`; }); } document.getElementById('cat-modal-body').innerHTML = html; const overlay = document.getElementById('cat-overlay'); overlay.style.display = 'flex'; } function closeCatModal(evt) { if (!evt || evt.target === document.getElementById('cat-overlay')) { document.getElementById('cat-overlay').style.display = 'none'; } } // ── BUDGET ZIELE ───────────────────────────────────────── var budgetZiele = []; var budgetEditId = null; async function loadBudgetZiele() { var res = await db.from('budgetziele').select('*').order('created_at', {ascending:true}); if (res.data) budgetZiele = res.data; renderBudgetZiele(); renderDashBudget(); } function showBudgetForm(id) { budgetEditId = id || null; var cats = CATS_OUT.map(function(c) { return ''; }).join(''); document.getElementById('budget-cat').innerHTML = cats; if (id) { var z = budgetZiele.find(function(x){ return x.id === id; }); if (z) { document.getElementById('budget-cat').value = z.kategorie; document.getElementById('budget-betrag').value = z.betrag; document.getElementById('budget-form-title').textContent = 'Ziel bearbeiten'; } } else { document.getElementById('budget-betrag').value = ''; document.getElementById('budget-form-title').textContent = 'Neues Budget-Ziel'; } document.getElementById('budget-form').style.display = 'block'; document.getElementById('budget-form').scrollIntoView({behavior:'smooth'}); } function hideBudgetForm() { document.getElementById('budget-form').style.display = 'none'; budgetEditId = null; } async function saveBudgetZiel() { var kat = document.getElementById('budget-cat').value; var betrag = parseFloat(document.getElementById('budget-betrag').value.toString().replace(',','.')); if (!kat || !betrag || betrag <= 0) { alert('Bitte Kategorie und Betrag angeben'); return; } var row = { kategorie: kat, betrag: betrag, typ: 'out' }; sync('Speichere…'); if (budgetEditId) { await db.from('budgetziele').update(row).eq('id', budgetEditId); var i = budgetZiele.findIndex(function(x){ return x.id === budgetEditId; }); if (i >= 0) budgetZiele[i] = Object.assign({}, budgetZiele[i], row); } else { var res = await db.from('budgetziele').insert([row]).select(); if (res.data) budgetZiele.push(res.data[0]); } hideBudgetForm(); renderBudgetZiele(); renderDashBudget(); unsync(); } async function deleteBudgetZiel(id) { if (!confirm('Budget-Ziel löschen?')) return; sync('Lösche…'); await db.from('budgetziele').delete().eq('id', id); budgetZiele = budgetZiele.filter(function(x){ return x.id !== id; }); renderBudgetZiele(); renderDashBudget(); unsync(); } function getBudgetSpend() { // Get current month spend per category var f = entries.filter(function(e) { var d = new Date(e.date); return e.type === 'out' && d.getMonth() === curMonth && d.getFullYear() === curYear; }); var spend = {}; f.forEach(function(e) { var cat = e.category || 'Sonstiges'; spend[cat] = (spend[cat] || 0) + parseFloat(e.amount); }); return spend; } function renderBudgetZiele() { var el = document.getElementById('budget-list'); if (!el) return; if (!budgetZiele.length) { el.innerHTML = '
Noch keine Budget-Ziele. Tippe auf "+ Neu" um eines anzulegen.
'; return; } var spend = getBudgetSpend(); var html = ''; budgetZiele.forEach(function(z) { var cat = z.kategorie; var limit = parseFloat(z.betrag); var used = spend[cat] || 0; var pct = Math.min(100, (used / limit * 100)); var over = used > limit; var color = over ? 'var(--red)' : pct > 80 ? '#f59e0b' : 'var(--green)'; var catColor = (CATS_OUT.find(function(c){ return c.l===cat; }) || {}).c || '#aaa'; html += '
'; html += '
'; html += '
'; html += '
'; html += ''+cat+''; html += '
'; html += '
'; html += ''; html += ''; html += '
'; html += '
'; html += 'Verbraucht'; html += ''+used.toLocaleString('de-DE',{minimumFractionDigits:2,maximumFractionDigits:2})+' €'; html += ' / '+limit.toLocaleString('de-DE',{minimumFractionDigits:2,maximumFractionDigits:2})+' €'; html += '
'; html += '
'; html += '
'; html += '
'; html += '
'; if (over) { html += '⚠️ '+(used-limit).toLocaleString('de-DE',{minimumFractionDigits:2,maximumFractionDigits:2})+' € überzogen'; } else { html += ''+(limit-used).toLocaleString('de-DE',{minimumFractionDigits:2,maximumFractionDigits:2})+' € noch verfügbar'; } html += ''+pct.toFixed(0)+'%'; html += '
'; }); el.innerHTML = html; } function renderDashBudget() { var el = document.getElementById('dash-budget-summary'); if (!el) return; if (!budgetZiele.length) { el.innerHTML = '
Noch keine Ziele definiert
'; return; } var spend = getBudgetSpend(); var html = ''; budgetZiele.forEach(function(z) { var cat = z.kategorie; var limit = parseFloat(z.betrag); var used = spend[cat] || 0; var pct = Math.min(100, (used / limit * 100)); var over = used > limit; var color = over ? 'var(--red)' : pct > 80 ? '#f59e0b' : 'var(--green)'; var catColor = (CATS_OUT.find(function(c){ return c.l===cat; }) || {}).c || '#aaa'; html += '
'; html += '
'; html += '
'; html += '
'; html += ''+cat+''; html += '
'; html += ''; html += used.toLocaleString('de-DE',{minimumFractionDigits:2,maximumFractionDigits:2})+' / '+limit.toLocaleString('de-DE',{minimumFractionDigits:2,maximumFractionDigits:2})+' €'; html += '
'; html += '
'; html += '
'; html += '
'; if (over) html += '
⚠️ '+(used-limit).toLocaleString('de-DE',{minimumFractionDigits:2,maximumFractionDigits:2})+' € überzogen
'; html += '
'; }); el.innerHTML = html; } // ── RATENKÄUFE ─────────────────────────────────────────── var ratenkaufe = []; var rkEditId = null; async function loadRatenkaufe() { try { var res = await db.from('ratenkaufe').select('*').order('created_at', {ascending:false}); if (res.data) ratenkaufe = res.data; } catch(e) { try { ratenkaufe = JSON.parse(localStorage.getItem('mb_rk')||'[]'); } catch(e2){ ratenkaufe=[]; } } renderRatenkaufe(); renderDashRatenkaufe(); } function editRatenkauf(id) { var r = ratenkaufe.find(function(x){ return String(x.id) === String(id); }); if (!r) return; rkEditId = id; document.getElementById('rk-form-title').textContent = 'Ratenkauf bearbeiten'; document.getElementById('rk-name').value = r.name || ''; document.getElementById('rk-cat').value = r.kategorie || 'Hardware'; document.getElementById('rk-gesamt').value = parseFloat(r.gesamt).toLocaleString('de-DE', {minimumFractionDigits:2, maximumFractionDigits:2}); document.getElementById('rk-anzahl').value = r.anzahl || ''; document.getElementById('rk-rate').value = parseFloat(r.rate).toLocaleString('de-DE', {minimumFractionDigits:2, maximumFractionDigits:2}); document.getElementById('rk-notiz').value = r.notiz || ''; // Format start date as MM.JJJJ if (r.start_datum) { var d = new Date(r.start_datum); document.getElementById('rk-start').value = String(d.getMonth()+1).padStart(2,'0') + '.' + d.getFullYear(); } calcRkRate(); document.getElementById('rk-form').style.display = 'block'; document.getElementById('rk-form').scrollIntoView({behavior:'smooth'}); } function showRatenkaufForm() { rkEditId = null; document.getElementById('rk-form-title').textContent = 'Neuer Ratenkauf'; document.getElementById('rk-name').value = ''; document.getElementById('rk-gesamt').value = ''; document.getElementById('rk-anzahl').value = ''; document.getElementById('rk-rate').value = ''; document.getElementById('rk-notiz').value = ''; document.getElementById('rk-start').value = '01.' + new Date().getFullYear(); document.getElementById('rk-form').style.display = 'block'; document.getElementById('rk-form').scrollIntoView({behavior:'smooth'}); } function calcRkRate() { var g = parseEuro(document.getElementById('rk-gesamt').value); var n = parseInt(document.getElementById('rk-anzahl').value||0); var startRaw = document.getElementById('rk-start').value.trim(); if (g > 0 && n > 0) { document.getElementById('rk-rate').value = (g/n).toLocaleString('de-DE',{minimumFractionDigits:2,maximumFractionDigits:2}); } // Show letzte Rate preview var letzteRow = document.getElementById('rk-letzte-row'); if (g > 0 && n > 0 && startRaw.length >= 6) { var parts = startRaw.split('.'); var startDate; if (parts.length === 2 && parts[1].length === 4) { startDate = new Date(parseInt(parts[1]), parseInt(parts[0])-1, 1); } else if (parts.length === 3 && parts[2].length === 4) { startDate = new Date(parseInt(parts[2]), parseInt(parts[1])-1, 1); } if (startDate && !isNaN(startDate)) { var endDate = new Date(startDate.getFullYear(), startDate.getMonth() + n - 1, 1); var now = new Date(); var elapsed = Math.max(0, (now.getFullYear()-startDate.getFullYear())*12 + (now.getMonth()-startDate.getMonth())); var bezahltRaten = Math.min(elapsed, n); var bezahltBetrag = bezahltRaten * (g/n); document.getElementById('rk-letzte-label').textContent = endDate.toLocaleDateString('de-DE',{month:'long',year:'numeric'}); document.getElementById('rk-bezahlt-label').textContent = bezahltRaten + ' von ' + n + ' Raten · ' + bezahltBetrag.toLocaleString('de-DE',{minimumFractionDigits:2,maximumFractionDigits:2}) + ' €'; letzteRow.style.display = 'block'; } else { letzteRow.style.display = 'none'; } } else { letzteRow.style.display = 'none'; } } function hideRatenkaufForm() { document.getElementById('rk-form').style.display = 'none'; rkEditId = null; } async function saveRatenkauf() { var name = document.getElementById('rk-name').value.trim(); var gesamt = parseEuro(document.getElementById('rk-gesamt').value); var anzahl = parseInt(document.getElementById('rk-anzahl').value||0); var startRaw = document.getElementById('rk-start').value.trim(); if (!name || !gesamt || !anzahl || !startRaw) { alert('Bitte alle Pflichtfelder ausfüllen'); return; } // Parse MM.JJJJ oder TT.MM.JJJJ var parts = startRaw.split('.'); var startDate; if (parts.length === 2 && parts[1].length === 4) { startDate = new Date(parseInt(parts[1]), parseInt(parts[0])-1, 1); } else if (parts.length === 3) { startDate = new Date(parseInt(parts[2]), parseInt(parts[1])-1, 1); } else { alert('Bitte Startdatum im Format MM.JJJJ eingeben, z.B. 01.2025'); return; } var start = startDate.getFullYear() + '-' + String(startDate.getMonth()+1).padStart(2,'0') + '-01'; var endDate = new Date(startDate.getFullYear(), startDate.getMonth() + anzahl, 1); var endStr = endDate.getFullYear() + '-' + String(endDate.getMonth()+1).padStart(2,'0') + '-01'; var rate = parseEuro(document.getElementById('rk-rate').value) || gesamt/anzahl; var row = { name: name, kategorie: document.getElementById('rk-cat').value, gesamt: gesamt, anzahl: anzahl, rate: rate, start_datum: start, end_datum: endStr, notiz: document.getElementById('rk-notiz').value.trim() }; sync('Speichere…'); try { if (rkEditId) { await db.from('ratenkaufe').update(row).eq('id', rkEditId); var i = ratenkaufe.findIndex(r=>r.id===rkEditId); if (i>=0) ratenkaufe[i] = Object.assign({}, ratenkaufe[i], row); } else { var res = await db.from('ratenkaufe').insert([row]).select(); if (res.data) ratenkaufe.unshift(res.data[0]); else { row.id = Date.now().toString(); ratenkaufe.unshift(row); } } } catch(e) { row.id = Date.now().toString(); ratenkaufe.unshift(row); } localStorage.setItem('mb_rk', JSON.stringify(ratenkaufe)); hideRatenkaufForm(); renderRatenkaufe(); unsync(); } async function deleteRatenkauf(id) { if (!confirm('Ratenkauf löschen?')) return; sync('Lösche…'); try { await db.from('ratenkaufe').delete().eq('id', id); } catch(e){} ratenkaufe = ratenkaufe.filter(r=>String(r.id)!==String(id)); localStorage.setItem('mb_rk', JSON.stringify(ratenkaufe)); renderRatenkaufe(); unsync(); } function renderRatenkaufe() { var now = new Date(); var aktiv = ratenkaufe.filter(r => new Date(r.end_datum) > now); var abgeschlossen = ratenkaufe.filter(r => new Date(r.end_datum) <= now); // Summary var totalMonatlich = aktiv.reduce((s,r)=>s+parseFloat(r.rate||0),0); var summaryEl = document.getElementById('rk-summary'); if (aktiv.length > 0) { summaryEl.innerHTML = `
MONATLICHE BELASTUNG
${fmt(totalMonatlich)}
${aktiv.length} aktive Rate${aktiv.length!==1?'n':''}
`; } else { summaryEl.innerHTML = ''; } var listEl = document.getElementById('rk-list'); if (!ratenkaufe.length) { listEl.innerHTML = '
Noch keine Ratenkäufe erfasst
'; return; } function renderCard(r, isAktiv) { var startDate = new Date(r.start_datum); var endDate = new Date(r.end_datum); var totalMonths = r.anzahl; var nowDate = new Date(); var elapsed = Math.max(0, (nowDate.getFullYear()-startDate.getFullYear())*12 + (nowDate.getMonth()-startDate.getMonth())); var remaining = Math.max(0, totalMonths - elapsed); var pct = Math.min(100, Math.round((elapsed/totalMonths)*100)); var bezahlt = Math.min(elapsed, totalMonths) * parseFloat(r.rate); var rest = parseFloat(r.gesamt) - bezahlt; var endLabel = endDate.toLocaleDateString('de-DE',{month:'long',year:'numeric'}); return `
${r.name}
${r.kategorie}${r.notiz?' · '+r.notiz:''}
${isAktiv?`aktiv`:`fertig`}
Gesamt
${fmt(parseFloat(r.gesamt))}
Rate/Monat
${fmt(parseFloat(r.rate))}
${isAktiv?'Noch':'Ende'}
${isAktiv?remaining+' Monate':endLabel}
Bezahlt: ${fmt(bezahlt)} ${pct}% Ende: ${endLabel}
`; } var html = ''; if (aktiv.length) { html += '
Laufend
'; html += aktiv.map(r=>renderCard(r,true)).join(''); } if (abgeschlossen.length) { html += '
Abgeschlossen
'; html += abgeschlossen.map(r=>renderCard(r,false)).join(''); } listEl.innerHTML = html; } function go(view) { const map={dash:"v-dash",add:"v-add",list:"v-list",darl:"v-darl",invest:"v-invest",raten:"v-raten",budget:"v-budget"}; const nmap={dash:"n-dash",add:"n-add",list:"n-list",darl:"n-darl",invest:"n-invest",raten:"n-raten",budget:"n-budget"}; Object.values(map).forEach(id=>document.getElementById(id).classList.remove("active")); Object.values(nmap).forEach(id=>document.getElementById(id).classList.remove("active")); document.getElementById(map[view]).classList.add("active"); document.getElementById(nmap[view]).classList.add("active"); if (view==="add") { editId=null; document.getElementById("form-title").textContent="Neue Buchung"; document.getElementById("save-btn").textContent="Speichern"; document.getElementById("datum").value=today(); document.getElementById("amount").value=""; document.getElementById("note").value=""; setType("out"); } if (view==="list") renderList(); if (view==="invest") { renderInvest(); fetchLivePrices(); } if (view==="raten") renderRatenkaufe(); if (view==="budget") { renderBudgetZiele(); renderDashBudget(); } } // ── INIT ───────────────────────────────────────────────── buildMonthSelect(); setType("out"); loadEntries(); loadDarlehen(); loadInvest(); loadRatenkaufe(); loadBudgetZiele(); fetchUsdEurRate(); +(balance/hebel/balance*100).toFixed(2)+'
'; // Auto-fill Notiz var notizEl = document.getElementById('iv-notiz'); if (notizEl && real > 0) { var roiPct = real > 0 ? (pnl/real*100).toFixed(2) : 0; var today = new Date().toLocaleDateString('de-DE',{day:'2-digit',month:'2-digit',year:'numeric'}); notizEl.value = 'CFD x'+hebel+' Hebel · Balance function setMvCurrency(cur) { mvCurrency = cur; var eurBtn = document.getElementById('iv-mv-eur'); var usdBtn = document.getElementById('iv-mv-usd'); var hint = document.getElementById('iv-mv-hint'); if (cur === 'EUR') { eurBtn.style.background = 'var(--blue)'; eurBtn.style.color = '#fff'; eurBtn.style.border = 'none'; usdBtn.style.background = 'var(--card2)'; usdBtn.style.color = '#aaa'; usdBtn.style.border = '1px solid var(--border)'; if (hint) hint.style.display = 'none'; } else { usdBtn.style.background = 'var(--blue)'; usdBtn.style.color = '#fff'; usdBtn.style.border = 'none'; eurBtn.style.background = 'var(--card2)'; eurBtn.style.color = '#aaa'; eurBtn.style.border = '1px solid var(--border)'; if (hint) { hint.style.display = 'block'; hint.textContent = '⏳ USD/EUR Kurs wird beim Speichern automatisch geladen…'; } } } async function saveInvestPos() { var name = document.getElementById('iv-name').value.trim(); var menge = parseFloat(document.getElementById('iv-menge').value.toString().replace(',','.')); var kaufpreis = getKaufpreisInEur(); if (!name || isNaN(menge) || isNaN(kaufpreis)) { alert('Bitte Name, Menge und Kaufpreis eingeben'); return; } // Handle USD -> EUR conversion for manual_val var rawMv = parseFloat(document.getElementById('iv-manual-val').value)||0; var manualVal = rawMv; if (mvCurrency === 'USD' && rawMv > 0) { var hint = document.getElementById('iv-mv-hint'); if (hint) hint.textContent = '⏳ USD/EUR Kurs wird geladen…'; var rate = usdEurRate || await fetchUsdEurRate(); manualVal = Math.round(rawMv * rate * 100) / 100; if (hint) hint.textContent = rawMv+' USD × '+rate.toFixed(4)+' = '+manualVal.toFixed(2)+' EUR ✓'; } var row = { name: name, typ: ivTyp, ticker: document.getElementById('iv-ticker').value.trim().toLowerCase(), depot: document.getElementById('iv-depot').value.trim(), menge: menge, kaufpreis: kaufpreis, manual_val: manualVal, notiz: document.getElementById('iv-notiz').value.trim() }; sync('Speichere…'); if (ivEditId) { await db.from('invest').update(row).eq('id', ivEditId); var i = investPositions.findIndex(function(p){ return p.id===ivEditId; }); if (i>=0) investPositions[i] = Object.assign({}, investPositions[i], row); } else { var res = await db.from('invest').insert([row]).select(); if (res.data) investPositions.unshift(res.data[0]); } closeInvestForm(); renderInvest(); fetchLivePrices(); unsync(); } async function deleteInvestPos(id) { if (!confirm('Position löschen?')) return; sync('Lösche…'); await db.from('invest').delete().eq('id', id); investPositions = investPositions.filter(function(p){ return p.id!==id; }); renderInvest(); renderDashInvest(); unsync(); } async function fetchLivePrices() { var cryptoTickers = []; var stockTickers = []; investPositions.forEach(function(p) { if ((p.typ==='crypto') && p.ticker) cryptoTickers.push(p.ticker); if ((p.typ==='stock'||p.typ==='etf') && p.ticker) stockTickers.push(p.ticker); }); // Fetch Crypto via CoinGecko with proxy fallbacks if (cryptoTickers.length > 0) { var ids = [...new Set(cryptoTickers)].join(','); var cgUrl = 'https://api.coingecko.com/api/v3/simple/price?ids='+ids+'&vs_currencies=eur'; var cgProxies = [ cgUrl, // direkt zuerst versuchen 'https://api.allorigins.win/raw?url='+encodeURIComponent(cgUrl), 'https://corsproxy.io/?'+encodeURIComponent(cgUrl), 'https://api.codetabs.com/v1/proxy?quest='+encodeURIComponent(cgUrl) ]; var cryptoFetched = false; for (var ci=0; ci 0) { Object.keys(priceData).forEach(function(k) { if (priceData[k] && priceData[k].eur) livePrices[k] = priceData[k].eur; }); cryptoFetched = true; console.log('Crypto fetched via proxy '+ci); } } catch(e) { console.log('Crypto proxy '+ci+' failed:', e.message); } } if (!cryptoFetched) console.log('All crypto proxies failed'); } // Fetch Stocks via Yahoo Finance with multiple proxies var uniqueStocks = [...new Set(stockTickers)]; for (var i=0; i 0) { if (currency === 'USD') price = price * 0.92; livePrices[sym.toLowerCase()] = price; fetched = true; console.log('Stock '+sym+' fetched: '+price+' via proxy'+pi+' url'+ui); } } catch(e) { console.log('Stock proxy'+pi+'/url'+ui+' failed for '+sym+':', e.message); } } } if (!fetched) console.log('All stock proxies failed for '+sym); } lastPriceUpdate = new Date(); renderInvest(); renderDashInvest(); } function getPrice(pos) { if (pos.typ === 'manual') return pos.manual_val || 0; var key = (pos.ticker || '').toLowerCase(); if (key && livePrices[key] !== undefined) return livePrices[key]; return null; // unknown } function renderInvest() { var depots = ['all']; investPositions.forEach(function(p) { if (p.depot && depots.indexOf(p.depot) === -1) depots.push(p.depot); }); // Depot filter buttons var filterEl = document.getElementById('iv-depot-filter'); if (filterEl) { filterEl.innerHTML = depots.map(function(d) { var active = d === ivDepotFilter; return ''; }).join(''); } var filtered = ivDepotFilter==='all' ? investPositions : investPositions.filter(function(p){ return p.depot===ivDepotFilter; }); var totalInvested = 0, totalCurrent = 0, unknownCount = 0; filtered.forEach(function(p) { var invested = (parseFloat(p.menge)||0) * (parseFloat(p.kaufpreis)||0); totalInvested += invested; var price = getPrice(p); if (price !== null) { totalCurrent += (parseFloat(p.menge)||0) * price; } else { unknownCount++; } }); var pnl = totalCurrent - totalInvested; var pnlPct = totalInvested > 0 ? (pnl/totalInvested*100) : 0; var pnlColor = pnl >= 0 ? 'var(--green)' : 'var(--red)'; var ivInv = document.getElementById('iv-invested'); var ivCur = document.getElementById('iv-current'); var ivPnl = document.getElementById('iv-pnl'); var ivPct = document.getElementById('iv-pnl-pct'); if (ivInv) ivInv.textContent = fmt(totalInvested); if (ivCur) ivCur.textContent = unknownCount>0 ? fmt(totalCurrent)+' *' : fmt(totalCurrent); if (ivPnl) { ivPnl.textContent = (pnl>=0?'+':'')+fmt(pnl); ivPnl.style.color=pnlColor; } if (ivPct) { ivPct.textContent = (pnlPct>=0?'+':'')+pnlPct.toFixed(2)+'%'; ivPct.style.color=pnlColor; } var listEl = document.getElementById('iv-list'); if (!listEl) return; if (filtered.length === 0) { listEl.innerHTML = '
Noch keine Positionen. Füge deine erste Position hinzu!
'; } else { var typIcons = {crypto:'₿', stock:'📊', etf:'📦', manual:'✍️'}; var typLabels = {crypto:'Crypto', stock:'Aktie', etf:'ETF', manual:'Manuell'}; listEl.innerHTML = filtered.map(function(p) { var invested = (parseFloat(p.menge)||0) * (parseFloat(p.kaufpreis)||0); var price = getPrice(p); var currentVal = price !== null ? (parseFloat(p.menge)||0) * price : null; var pnlPos = currentVal !== null ? currentVal - invested : null; var pnlPctPos = invested > 0 && pnlPos !== null ? (pnlPos/invested*100) : null; var pnlColor2 = pnlPos !== null && pnlPos >= 0 ? 'var(--green)' : 'var(--red)'; var priceStr = price !== null ? fmt(price) : 'wird geladen…'; var pnlStr = pnlPos !== null ? ''+(pnlPos>=0?'+':'')+fmt(pnlPos)+' '+(pnlPctPos>=0?'+':'')+pnlPctPos.toFixed(2)+'%' : ''; var html = ''; html += '
'; html += '
'; html += '
'; html += '
'+p.name+'
'; html += '
'+(typIcons[p.typ]||'')+' '+typLabels[p.typ]+(p.depot?' · '+p.depot:'')+(p.ticker?' · '+p.ticker.toUpperCase():'')+'
'; html += '
'; html += '
'; html += ''; html += ''; html += '
'; html += '
'; html += '
Menge
'+parseFloat(p.menge)+'
'; html += '
Ø Kauf
'+fmt(parseFloat(p.kaufpreis))+'
'; html += '
Kurs live
'+priceStr+'
'; html += '
'; html += '
'; // Row 1: Investiert vs Aktuell var currentVal = price !== null ? price * parseFloat(p.menge) : (p.typ === 'manual' ? parseFloat(p.manual_val||0) : null); html += '
'; html += '
Investiert: '+fmt(invested)+'
'; if (currentVal !== null) { html += '
Aktuell: '+fmt(currentVal)+'
'; } else { html += '
Aktuell: wird geladen…
'; } html += '
'; // Row 2: P&L html += '
'+pnlStr+'
'; html += '
'; // Notiz-Zeile html += '
'; html += '
'+(p.notiz ? p.notiz : 'Notiz hinzufügen…')+'
'; html += ''; html += '
'; // Manuelle Positionen: Quick-Wert-Update mit USD/EUR Toggle if (p.typ === "manual") { html += '
'; html += '
'; html += 'Akt. Wert:'; html += ''; html += ''; html += ''; html += '
'; html += ''; html += '
'; } html += '
'; return html; }).join(''); } var infoEl = document.getElementById('iv-update-info'); if (infoEl) { if (lastPriceUpdate) { var msg = 'Kurse: '+lastPriceUpdate.toLocaleTimeString('de-DE'); if (unknownCount > 0) msg += ' · '+unknownCount+' manuell'; infoEl.textContent = msg; infoEl.style.color = 'var(--green)'; } else { infoEl.textContent = 'Kurse konnten nicht geladen werden'; infoEl.style.color = 'var(--red)'; } } } async function ivRefreshPrices() { var btn = document.getElementById('iv-refresh-btn'); var info = document.getElementById('iv-update-info'); if (btn) { btn.textContent = '⏳ Laden…'; btn.disabled = true; } if (info) info.textContent = 'Kurse werden geladen…'; await fetchLivePrices(); if (btn) { btn.textContent = '↻ Aktualisieren'; btn.disabled = false; } } function setIvDepot(d) { ivDepotFilter = d; renderInvest(); } // Event delegation for invest list edit/delete buttons document.addEventListener('click', function(e) { var editId = e.target.getAttribute('data-edit-id'); var delId = e.target.getAttribute('data-del-id'); var qvId = e.target.getAttribute('data-qv-id'); var notizId = e.target.getAttribute('data-notiz-id'); var bzEdit = e.target.getAttribute('data-bz-edit'); var bzDel = e.target.getAttribute('data-bz-del'); if (editId) { openInvestForm(parseInt(editId)); } if (delId) { deleteInvestPos(parseInt(delId)); } if (qvId) { saveQuickVal(parseInt(qvId)); } if (notizId) { openNotizEdit(parseInt(notizId)); } if (bzEdit) { showBudgetForm(bzEdit); } if (bzDel) { deleteBudgetZiel(bzDel); } var qvCurId = e.target.getAttribute('data-qv-cur-id'); if (qvCurId) { toggleQvCurrency(qvCurId); } }); // USD/EUR state per position var qvCurrency = {}; // id -> 'EUR' or 'USD' var usdEurRate = null; async function fetchUsdEurRate() { try { var urls = [ 'https://api.allorigins.win/raw?url='+encodeURIComponent('https://api.frankfurter.app/latest?from=USD&to=EUR'), 'https://corsproxy.io/?'+encodeURIComponent('https://api.frankfurter.app/latest?from=USD&to=EUR'), 'https://api.frankfurter.app/latest?from=USD&to=EUR' ]; for (var i=0; i -1 && s.indexOf('.') > -1) { // Both: assume de-DE format: 2.855,46 s = s.replace(/\./g,'').replace(',','.'); } else { s = s.replace(',','.'); } return parseFloat(s) || 0; } // ── KATEGORIE MODAL ────────────────────────────────────── function openCatModal(type) { const f = filtered().filter(e => e.type === type); const cats = type === 'in' ? CATS_IN : CATS_OUT; const label = type === 'in' ? 'Einnahmen' : 'Ausgaben'; const total = f.reduce((s,e) => s + parseFloat(e.amount), 0); // Group by category const map = {}; f.forEach(e => { if (!map[e.category]) map[e.category] = []; map[e.category].push(e); }); // Sort categories by total descending const sorted = Object.entries(map).sort((a,b) => { const sa = a[1].reduce((s,e)=>s+parseFloat(e.amount),0); const sb = b[1].reduce((s,e)=>s+parseFloat(e.amount),0); return sb - sa; }); document.getElementById('cat-modal-title').textContent = label + ' · ' + MONTHS[curMonth] + ' ' + curYear; document.getElementById('cat-modal-sub').textContent = sorted.length + ' Kategorien · Gesamt: ' + fmt(total); let html = ''; if (!sorted.length) { html = '
Keine Einträge diesen Monat
'; } else { sorted.forEach(([cat, items]) => { const catTotal = items.reduce((s,e)=>s+parseFloat(e.amount),0); const pct = total > 0 ? (catTotal/total*100).toFixed(1) : 0; const color = cats.find(c=>c.l===cat)?.c || '#aaa'; const sortedItems = [...items].sort((a,b)=>new Date(b.date)-new Date(a.date)); html += `
${cat} ${pct}%
${type==='in'?'+':'-'}${fmt(catTotal)}
${sortedItems.map(e=>`
${e.note || '–'}
${e.date}
${fmt(parseFloat(e.amount))}
`).join('')}
`; }); } document.getElementById('cat-modal-body').innerHTML = html; const overlay = document.getElementById('cat-overlay'); overlay.style.display = 'flex'; } function closeCatModal(evt) { if (!evt || evt.target === document.getElementById('cat-overlay')) { document.getElementById('cat-overlay').style.display = 'none'; } } // ── BUDGET ZIELE ───────────────────────────────────────── var budgetZiele = []; var budgetEditId = null; async function loadBudgetZiele() { var res = await db.from('budgetziele').select('*').order('created_at', {ascending:true}); if (res.data) budgetZiele = res.data; renderBudgetZiele(); renderDashBudget(); } function showBudgetForm(id) { budgetEditId = id || null; var cats = CATS_OUT.map(function(c) { return ''; }).join(''); document.getElementById('budget-cat').innerHTML = cats; if (id) { var z = budgetZiele.find(function(x){ return x.id === id; }); if (z) { document.getElementById('budget-cat').value = z.kategorie; document.getElementById('budget-betrag').value = z.betrag; document.getElementById('budget-form-title').textContent = 'Ziel bearbeiten'; } } else { document.getElementById('budget-betrag').value = ''; document.getElementById('budget-form-title').textContent = 'Neues Budget-Ziel'; } document.getElementById('budget-form').style.display = 'block'; document.getElementById('budget-form').scrollIntoView({behavior:'smooth'}); } function hideBudgetForm() { document.getElementById('budget-form').style.display = 'none'; budgetEditId = null; } async function saveBudgetZiel() { var kat = document.getElementById('budget-cat').value; var betrag = parseFloat(document.getElementById('budget-betrag').value.toString().replace(',','.')); if (!kat || !betrag || betrag <= 0) { alert('Bitte Kategorie und Betrag angeben'); return; } var row = { kategorie: kat, betrag: betrag, typ: 'out' }; sync('Speichere…'); if (budgetEditId) { await db.from('budgetziele').update(row).eq('id', budgetEditId); var i = budgetZiele.findIndex(function(x){ return x.id === budgetEditId; }); if (i >= 0) budgetZiele[i] = Object.assign({}, budgetZiele[i], row); } else { var res = await db.from('budgetziele').insert([row]).select(); if (res.data) budgetZiele.push(res.data[0]); } hideBudgetForm(); renderBudgetZiele(); renderDashBudget(); unsync(); } async function deleteBudgetZiel(id) { if (!confirm('Budget-Ziel löschen?')) return; sync('Lösche…'); await db.from('budgetziele').delete().eq('id', id); budgetZiele = budgetZiele.filter(function(x){ return x.id !== id; }); renderBudgetZiele(); renderDashBudget(); unsync(); } function getBudgetSpend() { // Get current month spend per category var f = entries.filter(function(e) { var d = new Date(e.date); return e.type === 'out' && d.getMonth() === curMonth && d.getFullYear() === curYear; }); var spend = {}; f.forEach(function(e) { var cat = e.category || 'Sonstiges'; spend[cat] = (spend[cat] || 0) + parseFloat(e.amount); }); return spend; } function renderBudgetZiele() { var el = document.getElementById('budget-list'); if (!el) return; if (!budgetZiele.length) { el.innerHTML = '
Noch keine Budget-Ziele. Tippe auf "+ Neu" um eines anzulegen.
'; return; } var spend = getBudgetSpend(); var html = ''; budgetZiele.forEach(function(z) { var cat = z.kategorie; var limit = parseFloat(z.betrag); var used = spend[cat] || 0; var pct = Math.min(100, (used / limit * 100)); var over = used > limit; var color = over ? 'var(--red)' : pct > 80 ? '#f59e0b' : 'var(--green)'; var catColor = (CATS_OUT.find(function(c){ return c.l===cat; }) || {}).c || '#aaa'; html += '
'; html += '
'; html += '
'; html += '
'; html += ''+cat+''; html += '
'; html += '
'; html += ''; html += ''; html += '
'; html += '
'; html += 'Verbraucht'; html += ''+used.toLocaleString('de-DE',{minimumFractionDigits:2,maximumFractionDigits:2})+' €'; html += ' / '+limit.toLocaleString('de-DE',{minimumFractionDigits:2,maximumFractionDigits:2})+' €'; html += '
'; html += '
'; html += '
'; html += '
'; html += '
'; if (over) { html += '⚠️ '+(used-limit).toLocaleString('de-DE',{minimumFractionDigits:2,maximumFractionDigits:2})+' € überzogen'; } else { html += ''+(limit-used).toLocaleString('de-DE',{minimumFractionDigits:2,maximumFractionDigits:2})+' € noch verfügbar'; } html += ''+pct.toFixed(0)+'%'; html += '
'; }); el.innerHTML = html; } function renderDashBudget() { var el = document.getElementById('dash-budget-summary'); if (!el) return; if (!budgetZiele.length) { el.innerHTML = '
Noch keine Ziele definiert
'; return; } var spend = getBudgetSpend(); var html = ''; budgetZiele.forEach(function(z) { var cat = z.kategorie; var limit = parseFloat(z.betrag); var used = spend[cat] || 0; var pct = Math.min(100, (used / limit * 100)); var over = used > limit; var color = over ? 'var(--red)' : pct > 80 ? '#f59e0b' : 'var(--green)'; var catColor = (CATS_OUT.find(function(c){ return c.l===cat; }) || {}).c || '#aaa'; html += '
'; html += '
'; html += '
'; html += '
'; html += ''+cat+''; html += '
'; html += ''; html += used.toLocaleString('de-DE',{minimumFractionDigits:2,maximumFractionDigits:2})+' / '+limit.toLocaleString('de-DE',{minimumFractionDigits:2,maximumFractionDigits:2})+' €'; html += '
'; html += '
'; html += '
'; html += '
'; if (over) html += '
⚠️ '+(used-limit).toLocaleString('de-DE',{minimumFractionDigits:2,maximumFractionDigits:2})+' € überzogen
'; html += '
'; }); el.innerHTML = html; } // ── RATENKÄUFE ─────────────────────────────────────────── var ratenkaufe = []; var rkEditId = null; async function loadRatenkaufe() { try { var res = await db.from('ratenkaufe').select('*').order('created_at', {ascending:false}); if (res.data) ratenkaufe = res.data; } catch(e) { try { ratenkaufe = JSON.parse(localStorage.getItem('mb_rk')||'[]'); } catch(e2){ ratenkaufe=[]; } } renderRatenkaufe(); renderDashRatenkaufe(); } function editRatenkauf(id) { var r = ratenkaufe.find(function(x){ return String(x.id) === String(id); }); if (!r) return; rkEditId = id; document.getElementById('rk-form-title').textContent = 'Ratenkauf bearbeiten'; document.getElementById('rk-name').value = r.name || ''; document.getElementById('rk-cat').value = r.kategorie || 'Hardware'; document.getElementById('rk-gesamt').value = parseFloat(r.gesamt).toLocaleString('de-DE', {minimumFractionDigits:2, maximumFractionDigits:2}); document.getElementById('rk-anzahl').value = r.anzahl || ''; document.getElementById('rk-rate').value = parseFloat(r.rate).toLocaleString('de-DE', {minimumFractionDigits:2, maximumFractionDigits:2}); document.getElementById('rk-notiz').value = r.notiz || ''; // Format start date as MM.JJJJ if (r.start_datum) { var d = new Date(r.start_datum); document.getElementById('rk-start').value = String(d.getMonth()+1).padStart(2,'0') + '.' + d.getFullYear(); } calcRkRate(); document.getElementById('rk-form').style.display = 'block'; document.getElementById('rk-form').scrollIntoView({behavior:'smooth'}); } function showRatenkaufForm() { rkEditId = null; document.getElementById('rk-form-title').textContent = 'Neuer Ratenkauf'; document.getElementById('rk-name').value = ''; document.getElementById('rk-gesamt').value = ''; document.getElementById('rk-anzahl').value = ''; document.getElementById('rk-rate').value = ''; document.getElementById('rk-notiz').value = ''; document.getElementById('rk-start').value = '01.' + new Date().getFullYear(); document.getElementById('rk-form').style.display = 'block'; document.getElementById('rk-form').scrollIntoView({behavior:'smooth'}); } function calcRkRate() { var g = parseEuro(document.getElementById('rk-gesamt').value); var n = parseInt(document.getElementById('rk-anzahl').value||0); var startRaw = document.getElementById('rk-start').value.trim(); if (g > 0 && n > 0) { document.getElementById('rk-rate').value = (g/n).toLocaleString('de-DE',{minimumFractionDigits:2,maximumFractionDigits:2}); } // Show letzte Rate preview var letzteRow = document.getElementById('rk-letzte-row'); if (g > 0 && n > 0 && startRaw.length >= 6) { var parts = startRaw.split('.'); var startDate; if (parts.length === 2 && parts[1].length === 4) { startDate = new Date(parseInt(parts[1]), parseInt(parts[0])-1, 1); } else if (parts.length === 3 && parts[2].length === 4) { startDate = new Date(parseInt(parts[2]), parseInt(parts[1])-1, 1); } if (startDate && !isNaN(startDate)) { var endDate = new Date(startDate.getFullYear(), startDate.getMonth() + n - 1, 1); var now = new Date(); var elapsed = Math.max(0, (now.getFullYear()-startDate.getFullYear())*12 + (now.getMonth()-startDate.getMonth())); var bezahltRaten = Math.min(elapsed, n); var bezahltBetrag = bezahltRaten * (g/n); document.getElementById('rk-letzte-label').textContent = endDate.toLocaleDateString('de-DE',{month:'long',year:'numeric'}); document.getElementById('rk-bezahlt-label').textContent = bezahltRaten + ' von ' + n + ' Raten · ' + bezahltBetrag.toLocaleString('de-DE',{minimumFractionDigits:2,maximumFractionDigits:2}) + ' €'; letzteRow.style.display = 'block'; } else { letzteRow.style.display = 'none'; } } else { letzteRow.style.display = 'none'; } } function hideRatenkaufForm() { document.getElementById('rk-form').style.display = 'none'; rkEditId = null; } async function saveRatenkauf() { var name = document.getElementById('rk-name').value.trim(); var gesamt = parseEuro(document.getElementById('rk-gesamt').value); var anzahl = parseInt(document.getElementById('rk-anzahl').value||0); var startRaw = document.getElementById('rk-start').value.trim(); if (!name || !gesamt || !anzahl || !startRaw) { alert('Bitte alle Pflichtfelder ausfüllen'); return; } // Parse MM.JJJJ oder TT.MM.JJJJ var parts = startRaw.split('.'); var startDate; if (parts.length === 2 && parts[1].length === 4) { startDate = new Date(parseInt(parts[1]), parseInt(parts[0])-1, 1); } else if (parts.length === 3) { startDate = new Date(parseInt(parts[2]), parseInt(parts[1])-1, 1); } else { alert('Bitte Startdatum im Format MM.JJJJ eingeben, z.B. 01.2025'); return; } var start = startDate.getFullYear() + '-' + String(startDate.getMonth()+1).padStart(2,'0') + '-01'; var endDate = new Date(startDate.getFullYear(), startDate.getMonth() + anzahl, 1); var endStr = endDate.getFullYear() + '-' + String(endDate.getMonth()+1).padStart(2,'0') + '-01'; var rate = parseEuro(document.getElementById('rk-rate').value) || gesamt/anzahl; var row = { name: name, kategorie: document.getElementById('rk-cat').value, gesamt: gesamt, anzahl: anzahl, rate: rate, start_datum: start, end_datum: endStr, notiz: document.getElementById('rk-notiz').value.trim() }; sync('Speichere…'); try { if (rkEditId) { await db.from('ratenkaufe').update(row).eq('id', rkEditId); var i = ratenkaufe.findIndex(r=>r.id===rkEditId); if (i>=0) ratenkaufe[i] = Object.assign({}, ratenkaufe[i], row); } else { var res = await db.from('ratenkaufe').insert([row]).select(); if (res.data) ratenkaufe.unshift(res.data[0]); else { row.id = Date.now().toString(); ratenkaufe.unshift(row); } } } catch(e) { row.id = Date.now().toString(); ratenkaufe.unshift(row); } localStorage.setItem('mb_rk', JSON.stringify(ratenkaufe)); hideRatenkaufForm(); renderRatenkaufe(); unsync(); } async function deleteRatenkauf(id) { if (!confirm('Ratenkauf löschen?')) return; sync('Lösche…'); try { await db.from('ratenkaufe').delete().eq('id', id); } catch(e){} ratenkaufe = ratenkaufe.filter(r=>String(r.id)!==String(id)); localStorage.setItem('mb_rk', JSON.stringify(ratenkaufe)); renderRatenkaufe(); unsync(); } function renderRatenkaufe() { var now = new Date(); var aktiv = ratenkaufe.filter(r => new Date(r.end_datum) > now); var abgeschlossen = ratenkaufe.filter(r => new Date(r.end_datum) <= now); // Summary var totalMonatlich = aktiv.reduce((s,r)=>s+parseFloat(r.rate||0),0); var summaryEl = document.getElementById('rk-summary'); if (aktiv.length > 0) { summaryEl.innerHTML = `
MONATLICHE BELASTUNG
${fmt(totalMonatlich)}
${aktiv.length} aktive Rate${aktiv.length!==1?'n':''}
`; } else { summaryEl.innerHTML = ''; } var listEl = document.getElementById('rk-list'); if (!ratenkaufe.length) { listEl.innerHTML = '
Noch keine Ratenkäufe erfasst
'; return; } function renderCard(r, isAktiv) { var startDate = new Date(r.start_datum); var endDate = new Date(r.end_datum); var totalMonths = r.anzahl; var nowDate = new Date(); var elapsed = Math.max(0, (nowDate.getFullYear()-startDate.getFullYear())*12 + (nowDate.getMonth()-startDate.getMonth())); var remaining = Math.max(0, totalMonths - elapsed); var pct = Math.min(100, Math.round((elapsed/totalMonths)*100)); var bezahlt = Math.min(elapsed, totalMonths) * parseFloat(r.rate); var rest = parseFloat(r.gesamt) - bezahlt; var endLabel = endDate.toLocaleDateString('de-DE',{month:'long',year:'numeric'}); return `
${r.name}
${r.kategorie}${r.notiz?' · '+r.notiz:''}
${isAktiv?`aktiv`:`fertig`}
Gesamt
${fmt(parseFloat(r.gesamt))}
Rate/Monat
${fmt(parseFloat(r.rate))}
${isAktiv?'Noch':'Ende'}
${isAktiv?remaining+' Monate':endLabel}
Bezahlt: ${fmt(bezahlt)} ${pct}% Ende: ${endLabel}
`; } var html = ''; if (aktiv.length) { html += '
Laufend
'; html += aktiv.map(r=>renderCard(r,true)).join(''); } if (abgeschlossen.length) { html += '
Abgeschlossen
'; html += abgeschlossen.map(r=>renderCard(r,false)).join(''); } listEl.innerHTML = html; } function go(view) { const map={dash:"v-dash",add:"v-add",list:"v-list",darl:"v-darl",invest:"v-invest",raten:"v-raten",budget:"v-budget"}; const nmap={dash:"n-dash",add:"n-add",list:"n-list",darl:"n-darl",invest:"n-invest",raten:"n-raten",budget:"n-budget"}; Object.values(map).forEach(id=>document.getElementById(id).classList.remove("active")); Object.values(nmap).forEach(id=>document.getElementById(id).classList.remove("active")); document.getElementById(map[view]).classList.add("active"); document.getElementById(nmap[view]).classList.add("active"); if (view==="add") { editId=null; document.getElementById("form-title").textContent="Neue Buchung"; document.getElementById("save-btn").textContent="Speichern"; document.getElementById("datum").value=today(); document.getElementById("amount").value=""; document.getElementById("note").value=""; setType("out"); } if (view==="list") renderList(); if (view==="invest") { renderInvest(); fetchLivePrices(); } if (view==="raten") renderRatenkaufe(); if (view==="budget") { renderBudgetZiele(); renderDashBudget(); } } // ── INIT ───────────────────────────────────────────────── buildMonthSelect(); setType("out"); loadEntries(); loadDarlehen(); loadInvest(); loadRatenkaufe(); loadBudgetZiele(); fetchUsdEurRate(); +balance.toFixed(2)+'· PnL + function setMvCurrency(cur) { mvCurrency = cur; var eurBtn = document.getElementById('iv-mv-eur'); var usdBtn = document.getElementById('iv-mv-usd'); var hint = document.getElementById('iv-mv-hint'); if (cur === 'EUR') { eurBtn.style.background = 'var(--blue)'; eurBtn.style.color = '#fff'; eurBtn.style.border = 'none'; usdBtn.style.background = 'var(--card2)'; usdBtn.style.color = '#aaa'; usdBtn.style.border = '1px solid var(--border)'; if (hint) hint.style.display = 'none'; } else { usdBtn.style.background = 'var(--blue)'; usdBtn.style.color = '#fff'; usdBtn.style.border = 'none'; eurBtn.style.background = 'var(--card2)'; eurBtn.style.color = '#aaa'; eurBtn.style.border = '1px solid var(--border)'; if (hint) { hint.style.display = 'block'; hint.textContent = '⏳ USD/EUR Kurs wird beim Speichern automatisch geladen…'; } } } async function saveInvestPos() { var name = document.getElementById('iv-name').value.trim(); var menge = parseFloat(document.getElementById('iv-menge').value.toString().replace(',','.')); var kaufpreis = getKaufpreisInEur(); if (!name || isNaN(menge) || isNaN(kaufpreis)) { alert('Bitte Name, Menge und Kaufpreis eingeben'); return; } // Handle USD -> EUR conversion for manual_val var rawMv = parseFloat(document.getElementById('iv-manual-val').value)||0; var manualVal = rawMv; if (mvCurrency === 'USD' && rawMv > 0) { var hint = document.getElementById('iv-mv-hint'); if (hint) hint.textContent = '⏳ USD/EUR Kurs wird geladen…'; var rate = usdEurRate || await fetchUsdEurRate(); manualVal = Math.round(rawMv * rate * 100) / 100; if (hint) hint.textContent = rawMv+' USD × '+rate.toFixed(4)+' = '+manualVal.toFixed(2)+' EUR ✓'; } var row = { name: name, typ: ivTyp, ticker: document.getElementById('iv-ticker').value.trim().toLowerCase(), depot: document.getElementById('iv-depot').value.trim(), menge: menge, kaufpreis: kaufpreis, manual_val: manualVal, notiz: document.getElementById('iv-notiz').value.trim() }; sync('Speichere…'); if (ivEditId) { await db.from('invest').update(row).eq('id', ivEditId); var i = investPositions.findIndex(function(p){ return p.id===ivEditId; }); if (i>=0) investPositions[i] = Object.assign({}, investPositions[i], row); } else { var res = await db.from('invest').insert([row]).select(); if (res.data) investPositions.unshift(res.data[0]); } closeInvestForm(); renderInvest(); fetchLivePrices(); unsync(); } async function deleteInvestPos(id) { if (!confirm('Position löschen?')) return; sync('Lösche…'); await db.from('invest').delete().eq('id', id); investPositions = investPositions.filter(function(p){ return p.id!==id; }); renderInvest(); renderDashInvest(); unsync(); } async function fetchLivePrices() { var cryptoTickers = []; var stockTickers = []; investPositions.forEach(function(p) { if ((p.typ==='crypto') && p.ticker) cryptoTickers.push(p.ticker); if ((p.typ==='stock'||p.typ==='etf') && p.ticker) stockTickers.push(p.ticker); }); // Fetch Crypto via CoinGecko with proxy fallbacks if (cryptoTickers.length > 0) { var ids = [...new Set(cryptoTickers)].join(','); var cgUrl = 'https://api.coingecko.com/api/v3/simple/price?ids='+ids+'&vs_currencies=eur'; var cgProxies = [ cgUrl, // direkt zuerst versuchen 'https://api.allorigins.win/raw?url='+encodeURIComponent(cgUrl), 'https://corsproxy.io/?'+encodeURIComponent(cgUrl), 'https://api.codetabs.com/v1/proxy?quest='+encodeURIComponent(cgUrl) ]; var cryptoFetched = false; for (var ci=0; ci 0) { Object.keys(priceData).forEach(function(k) { if (priceData[k] && priceData[k].eur) livePrices[k] = priceData[k].eur; }); cryptoFetched = true; console.log('Crypto fetched via proxy '+ci); } } catch(e) { console.log('Crypto proxy '+ci+' failed:', e.message); } } if (!cryptoFetched) console.log('All crypto proxies failed'); } // Fetch Stocks via Yahoo Finance with multiple proxies var uniqueStocks = [...new Set(stockTickers)]; for (var i=0; i 0) { if (currency === 'USD') price = price * 0.92; livePrices[sym.toLowerCase()] = price; fetched = true; console.log('Stock '+sym+' fetched: '+price+' via proxy'+pi+' url'+ui); } } catch(e) { console.log('Stock proxy'+pi+'/url'+ui+' failed for '+sym+':', e.message); } } } if (!fetched) console.log('All stock proxies failed for '+sym); } lastPriceUpdate = new Date(); renderInvest(); renderDashInvest(); } function getPrice(pos) { if (pos.typ === 'manual') return pos.manual_val || 0; var key = (pos.ticker || '').toLowerCase(); if (key && livePrices[key] !== undefined) return livePrices[key]; return null; // unknown } function renderInvest() { var depots = ['all']; investPositions.forEach(function(p) { if (p.depot && depots.indexOf(p.depot) === -1) depots.push(p.depot); }); // Depot filter buttons var filterEl = document.getElementById('iv-depot-filter'); if (filterEl) { filterEl.innerHTML = depots.map(function(d) { var active = d === ivDepotFilter; return ''; }).join(''); } var filtered = ivDepotFilter==='all' ? investPositions : investPositions.filter(function(p){ return p.depot===ivDepotFilter; }); var totalInvested = 0, totalCurrent = 0, unknownCount = 0; filtered.forEach(function(p) { var invested = (parseFloat(p.menge)||0) * (parseFloat(p.kaufpreis)||0); totalInvested += invested; var price = getPrice(p); if (price !== null) { totalCurrent += (parseFloat(p.menge)||0) * price; } else { unknownCount++; } }); var pnl = totalCurrent - totalInvested; var pnlPct = totalInvested > 0 ? (pnl/totalInvested*100) : 0; var pnlColor = pnl >= 0 ? 'var(--green)' : 'var(--red)'; var ivInv = document.getElementById('iv-invested'); var ivCur = document.getElementById('iv-current'); var ivPnl = document.getElementById('iv-pnl'); var ivPct = document.getElementById('iv-pnl-pct'); if (ivInv) ivInv.textContent = fmt(totalInvested); if (ivCur) ivCur.textContent = unknownCount>0 ? fmt(totalCurrent)+' *' : fmt(totalCurrent); if (ivPnl) { ivPnl.textContent = (pnl>=0?'+':'')+fmt(pnl); ivPnl.style.color=pnlColor; } if (ivPct) { ivPct.textContent = (pnlPct>=0?'+':'')+pnlPct.toFixed(2)+'%'; ivPct.style.color=pnlColor; } var listEl = document.getElementById('iv-list'); if (!listEl) return; if (filtered.length === 0) { listEl.innerHTML = '
Noch keine Positionen. Füge deine erste Position hinzu!
'; } else { var typIcons = {crypto:'₿', stock:'📊', etf:'📦', manual:'✍️'}; var typLabels = {crypto:'Crypto', stock:'Aktie', etf:'ETF', manual:'Manuell'}; listEl.innerHTML = filtered.map(function(p) { var invested = (parseFloat(p.menge)||0) * (parseFloat(p.kaufpreis)||0); var price = getPrice(p); var currentVal = price !== null ? (parseFloat(p.menge)||0) * price : null; var pnlPos = currentVal !== null ? currentVal - invested : null; var pnlPctPos = invested > 0 && pnlPos !== null ? (pnlPos/invested*100) : null; var pnlColor2 = pnlPos !== null && pnlPos >= 0 ? 'var(--green)' : 'var(--red)'; var priceStr = price !== null ? fmt(price) : 'wird geladen…'; var pnlStr = pnlPos !== null ? ''+(pnlPos>=0?'+':'')+fmt(pnlPos)+' '+(pnlPctPos>=0?'+':'')+pnlPctPos.toFixed(2)+'%' : ''; var html = ''; html += '
'; html += '
'; html += '
'; html += '
'+p.name+'
'; html += '
'+(typIcons[p.typ]||'')+' '+typLabels[p.typ]+(p.depot?' · '+p.depot:'')+(p.ticker?' · '+p.ticker.toUpperCase():'')+'
'; html += '
'; html += '
'; html += ''; html += ''; html += '
'; html += '
'; html += '
Menge
'+parseFloat(p.menge)+'
'; html += '
Ø Kauf
'+fmt(parseFloat(p.kaufpreis))+'
'; html += '
Kurs live
'+priceStr+'
'; html += '
'; html += '
'; // Row 1: Investiert vs Aktuell var currentVal = price !== null ? price * parseFloat(p.menge) : (p.typ === 'manual' ? parseFloat(p.manual_val||0) : null); html += '
'; html += '
Investiert: '+fmt(invested)+'
'; if (currentVal !== null) { html += '
Aktuell: '+fmt(currentVal)+'
'; } else { html += '
Aktuell: wird geladen…
'; } html += '
'; // Row 2: P&L html += '
'+pnlStr+'
'; html += '
'; // Notiz-Zeile html += '
'; html += '
'+(p.notiz ? p.notiz : 'Notiz hinzufügen…')+'
'; html += ''; html += '
'; // Manuelle Positionen: Quick-Wert-Update mit USD/EUR Toggle if (p.typ === "manual") { html += '
'; html += '
'; html += 'Akt. Wert:'; html += ''; html += ''; html += ''; html += '
'; html += ''; html += '
'; } html += '
'; return html; }).join(''); } var infoEl = document.getElementById('iv-update-info'); if (infoEl) { if (lastPriceUpdate) { var msg = 'Kurse: '+lastPriceUpdate.toLocaleTimeString('de-DE'); if (unknownCount > 0) msg += ' · '+unknownCount+' manuell'; infoEl.textContent = msg; infoEl.style.color = 'var(--green)'; } else { infoEl.textContent = 'Kurse konnten nicht geladen werden'; infoEl.style.color = 'var(--red)'; } } } async function ivRefreshPrices() { var btn = document.getElementById('iv-refresh-btn'); var info = document.getElementById('iv-update-info'); if (btn) { btn.textContent = '⏳ Laden…'; btn.disabled = true; } if (info) info.textContent = 'Kurse werden geladen…'; await fetchLivePrices(); if (btn) { btn.textContent = '↻ Aktualisieren'; btn.disabled = false; } } function setIvDepot(d) { ivDepotFilter = d; renderInvest(); } // Event delegation for invest list edit/delete buttons document.addEventListener('click', function(e) { var editId = e.target.getAttribute('data-edit-id'); var delId = e.target.getAttribute('data-del-id'); var qvId = e.target.getAttribute('data-qv-id'); var notizId = e.target.getAttribute('data-notiz-id'); var bzEdit = e.target.getAttribute('data-bz-edit'); var bzDel = e.target.getAttribute('data-bz-del'); if (editId) { openInvestForm(parseInt(editId)); } if (delId) { deleteInvestPos(parseInt(delId)); } if (qvId) { saveQuickVal(parseInt(qvId)); } if (notizId) { openNotizEdit(parseInt(notizId)); } if (bzEdit) { showBudgetForm(bzEdit); } if (bzDel) { deleteBudgetZiel(bzDel); } var qvCurId = e.target.getAttribute('data-qv-cur-id'); if (qvCurId) { toggleQvCurrency(qvCurId); } }); // USD/EUR state per position var qvCurrency = {}; // id -> 'EUR' or 'USD' var usdEurRate = null; async function fetchUsdEurRate() { try { var urls = [ 'https://api.allorigins.win/raw?url='+encodeURIComponent('https://api.frankfurter.app/latest?from=USD&to=EUR'), 'https://corsproxy.io/?'+encodeURIComponent('https://api.frankfurter.app/latest?from=USD&to=EUR'), 'https://api.frankfurter.app/latest?from=USD&to=EUR' ]; for (var i=0; i -1 && s.indexOf('.') > -1) { // Both: assume de-DE format: 2.855,46 s = s.replace(/\./g,'').replace(',','.'); } else { s = s.replace(',','.'); } return parseFloat(s) || 0; } // ── KATEGORIE MODAL ────────────────────────────────────── function openCatModal(type) { const f = filtered().filter(e => e.type === type); const cats = type === 'in' ? CATS_IN : CATS_OUT; const label = type === 'in' ? 'Einnahmen' : 'Ausgaben'; const total = f.reduce((s,e) => s + parseFloat(e.amount), 0); // Group by category const map = {}; f.forEach(e => { if (!map[e.category]) map[e.category] = []; map[e.category].push(e); }); // Sort categories by total descending const sorted = Object.entries(map).sort((a,b) => { const sa = a[1].reduce((s,e)=>s+parseFloat(e.amount),0); const sb = b[1].reduce((s,e)=>s+parseFloat(e.amount),0); return sb - sa; }); document.getElementById('cat-modal-title').textContent = label + ' · ' + MONTHS[curMonth] + ' ' + curYear; document.getElementById('cat-modal-sub').textContent = sorted.length + ' Kategorien · Gesamt: ' + fmt(total); let html = ''; if (!sorted.length) { html = '
Keine Einträge diesen Monat
'; } else { sorted.forEach(([cat, items]) => { const catTotal = items.reduce((s,e)=>s+parseFloat(e.amount),0); const pct = total > 0 ? (catTotal/total*100).toFixed(1) : 0; const color = cats.find(c=>c.l===cat)?.c || '#aaa'; const sortedItems = [...items].sort((a,b)=>new Date(b.date)-new Date(a.date)); html += `
${cat} ${pct}%
${type==='in'?'+':'-'}${fmt(catTotal)}
${sortedItems.map(e=>`
${e.note || '–'}
${e.date}
${fmt(parseFloat(e.amount))}
`).join('')}
`; }); } document.getElementById('cat-modal-body').innerHTML = html; const overlay = document.getElementById('cat-overlay'); overlay.style.display = 'flex'; } function closeCatModal(evt) { if (!evt || evt.target === document.getElementById('cat-overlay')) { document.getElementById('cat-overlay').style.display = 'none'; } } // ── BUDGET ZIELE ───────────────────────────────────────── var budgetZiele = []; var budgetEditId = null; async function loadBudgetZiele() { var res = await db.from('budgetziele').select('*').order('created_at', {ascending:true}); if (res.data) budgetZiele = res.data; renderBudgetZiele(); renderDashBudget(); } function showBudgetForm(id) { budgetEditId = id || null; var cats = CATS_OUT.map(function(c) { return ''; }).join(''); document.getElementById('budget-cat').innerHTML = cats; if (id) { var z = budgetZiele.find(function(x){ return x.id === id; }); if (z) { document.getElementById('budget-cat').value = z.kategorie; document.getElementById('budget-betrag').value = z.betrag; document.getElementById('budget-form-title').textContent = 'Ziel bearbeiten'; } } else { document.getElementById('budget-betrag').value = ''; document.getElementById('budget-form-title').textContent = 'Neues Budget-Ziel'; } document.getElementById('budget-form').style.display = 'block'; document.getElementById('budget-form').scrollIntoView({behavior:'smooth'}); } function hideBudgetForm() { document.getElementById('budget-form').style.display = 'none'; budgetEditId = null; } async function saveBudgetZiel() { var kat = document.getElementById('budget-cat').value; var betrag = parseFloat(document.getElementById('budget-betrag').value.toString().replace(',','.')); if (!kat || !betrag || betrag <= 0) { alert('Bitte Kategorie und Betrag angeben'); return; } var row = { kategorie: kat, betrag: betrag, typ: 'out' }; sync('Speichere…'); if (budgetEditId) { await db.from('budgetziele').update(row).eq('id', budgetEditId); var i = budgetZiele.findIndex(function(x){ return x.id === budgetEditId; }); if (i >= 0) budgetZiele[i] = Object.assign({}, budgetZiele[i], row); } else { var res = await db.from('budgetziele').insert([row]).select(); if (res.data) budgetZiele.push(res.data[0]); } hideBudgetForm(); renderBudgetZiele(); renderDashBudget(); unsync(); } async function deleteBudgetZiel(id) { if (!confirm('Budget-Ziel löschen?')) return; sync('Lösche…'); await db.from('budgetziele').delete().eq('id', id); budgetZiele = budgetZiele.filter(function(x){ return x.id !== id; }); renderBudgetZiele(); renderDashBudget(); unsync(); } function getBudgetSpend() { // Get current month spend per category var f = entries.filter(function(e) { var d = new Date(e.date); return e.type === 'out' && d.getMonth() === curMonth && d.getFullYear() === curYear; }); var spend = {}; f.forEach(function(e) { var cat = e.category || 'Sonstiges'; spend[cat] = (spend[cat] || 0) + parseFloat(e.amount); }); return spend; } function renderBudgetZiele() { var el = document.getElementById('budget-list'); if (!el) return; if (!budgetZiele.length) { el.innerHTML = '
Noch keine Budget-Ziele. Tippe auf "+ Neu" um eines anzulegen.
'; return; } var spend = getBudgetSpend(); var html = ''; budgetZiele.forEach(function(z) { var cat = z.kategorie; var limit = parseFloat(z.betrag); var used = spend[cat] || 0; var pct = Math.min(100, (used / limit * 100)); var over = used > limit; var color = over ? 'var(--red)' : pct > 80 ? '#f59e0b' : 'var(--green)'; var catColor = (CATS_OUT.find(function(c){ return c.l===cat; }) || {}).c || '#aaa'; html += '
'; html += '
'; html += '
'; html += '
'; html += ''+cat+''; html += '
'; html += '
'; html += ''; html += ''; html += '
'; html += '
'; html += 'Verbraucht'; html += ''+used.toLocaleString('de-DE',{minimumFractionDigits:2,maximumFractionDigits:2})+' €'; html += ' / '+limit.toLocaleString('de-DE',{minimumFractionDigits:2,maximumFractionDigits:2})+' €'; html += '
'; html += '
'; html += '
'; html += '
'; html += '
'; if (over) { html += '⚠️ '+(used-limit).toLocaleString('de-DE',{minimumFractionDigits:2,maximumFractionDigits:2})+' € überzogen'; } else { html += ''+(limit-used).toLocaleString('de-DE',{minimumFractionDigits:2,maximumFractionDigits:2})+' € noch verfügbar'; } html += ''+pct.toFixed(0)+'%'; html += '
'; }); el.innerHTML = html; } function renderDashBudget() { var el = document.getElementById('dash-budget-summary'); if (!el) return; if (!budgetZiele.length) { el.innerHTML = '
Noch keine Ziele definiert
'; return; } var spend = getBudgetSpend(); var html = ''; budgetZiele.forEach(function(z) { var cat = z.kategorie; var limit = parseFloat(z.betrag); var used = spend[cat] || 0; var pct = Math.min(100, (used / limit * 100)); var over = used > limit; var color = over ? 'var(--red)' : pct > 80 ? '#f59e0b' : 'var(--green)'; var catColor = (CATS_OUT.find(function(c){ return c.l===cat; }) || {}).c || '#aaa'; html += '
'; html += '
'; html += '
'; html += '
'; html += ''+cat+''; html += '
'; html += ''; html += used.toLocaleString('de-DE',{minimumFractionDigits:2,maximumFractionDigits:2})+' / '+limit.toLocaleString('de-DE',{minimumFractionDigits:2,maximumFractionDigits:2})+' €'; html += '
'; html += '
'; html += '
'; html += '
'; if (over) html += '
⚠️ '+(used-limit).toLocaleString('de-DE',{minimumFractionDigits:2,maximumFractionDigits:2})+' € überzogen
'; html += '
'; }); el.innerHTML = html; } // ── RATENKÄUFE ─────────────────────────────────────────── var ratenkaufe = []; var rkEditId = null; async function loadRatenkaufe() { try { var res = await db.from('ratenkaufe').select('*').order('created_at', {ascending:false}); if (res.data) ratenkaufe = res.data; } catch(e) { try { ratenkaufe = JSON.parse(localStorage.getItem('mb_rk')||'[]'); } catch(e2){ ratenkaufe=[]; } } renderRatenkaufe(); renderDashRatenkaufe(); } function editRatenkauf(id) { var r = ratenkaufe.find(function(x){ return String(x.id) === String(id); }); if (!r) return; rkEditId = id; document.getElementById('rk-form-title').textContent = 'Ratenkauf bearbeiten'; document.getElementById('rk-name').value = r.name || ''; document.getElementById('rk-cat').value = r.kategorie || 'Hardware'; document.getElementById('rk-gesamt').value = parseFloat(r.gesamt).toLocaleString('de-DE', {minimumFractionDigits:2, maximumFractionDigits:2}); document.getElementById('rk-anzahl').value = r.anzahl || ''; document.getElementById('rk-rate').value = parseFloat(r.rate).toLocaleString('de-DE', {minimumFractionDigits:2, maximumFractionDigits:2}); document.getElementById('rk-notiz').value = r.notiz || ''; // Format start date as MM.JJJJ if (r.start_datum) { var d = new Date(r.start_datum); document.getElementById('rk-start').value = String(d.getMonth()+1).padStart(2,'0') + '.' + d.getFullYear(); } calcRkRate(); document.getElementById('rk-form').style.display = 'block'; document.getElementById('rk-form').scrollIntoView({behavior:'smooth'}); } function showRatenkaufForm() { rkEditId = null; document.getElementById('rk-form-title').textContent = 'Neuer Ratenkauf'; document.getElementById('rk-name').value = ''; document.getElementById('rk-gesamt').value = ''; document.getElementById('rk-anzahl').value = ''; document.getElementById('rk-rate').value = ''; document.getElementById('rk-notiz').value = ''; document.getElementById('rk-start').value = '01.' + new Date().getFullYear(); document.getElementById('rk-form').style.display = 'block'; document.getElementById('rk-form').scrollIntoView({behavior:'smooth'}); } function calcRkRate() { var g = parseEuro(document.getElementById('rk-gesamt').value); var n = parseInt(document.getElementById('rk-anzahl').value||0); var startRaw = document.getElementById('rk-start').value.trim(); if (g > 0 && n > 0) { document.getElementById('rk-rate').value = (g/n).toLocaleString('de-DE',{minimumFractionDigits:2,maximumFractionDigits:2}); } // Show letzte Rate preview var letzteRow = document.getElementById('rk-letzte-row'); if (g > 0 && n > 0 && startRaw.length >= 6) { var parts = startRaw.split('.'); var startDate; if (parts.length === 2 && parts[1].length === 4) { startDate = new Date(parseInt(parts[1]), parseInt(parts[0])-1, 1); } else if (parts.length === 3 && parts[2].length === 4) { startDate = new Date(parseInt(parts[2]), parseInt(parts[1])-1, 1); } if (startDate && !isNaN(startDate)) { var endDate = new Date(startDate.getFullYear(), startDate.getMonth() + n - 1, 1); var now = new Date(); var elapsed = Math.max(0, (now.getFullYear()-startDate.getFullYear())*12 + (now.getMonth()-startDate.getMonth())); var bezahltRaten = Math.min(elapsed, n); var bezahltBetrag = bezahltRaten * (g/n); document.getElementById('rk-letzte-label').textContent = endDate.toLocaleDateString('de-DE',{month:'long',year:'numeric'}); document.getElementById('rk-bezahlt-label').textContent = bezahltRaten + ' von ' + n + ' Raten · ' + bezahltBetrag.toLocaleString('de-DE',{minimumFractionDigits:2,maximumFractionDigits:2}) + ' €'; letzteRow.style.display = 'block'; } else { letzteRow.style.display = 'none'; } } else { letzteRow.style.display = 'none'; } } function hideRatenkaufForm() { document.getElementById('rk-form').style.display = 'none'; rkEditId = null; } async function saveRatenkauf() { var name = document.getElementById('rk-name').value.trim(); var gesamt = parseEuro(document.getElementById('rk-gesamt').value); var anzahl = parseInt(document.getElementById('rk-anzahl').value||0); var startRaw = document.getElementById('rk-start').value.trim(); if (!name || !gesamt || !anzahl || !startRaw) { alert('Bitte alle Pflichtfelder ausfüllen'); return; } // Parse MM.JJJJ oder TT.MM.JJJJ var parts = startRaw.split('.'); var startDate; if (parts.length === 2 && parts[1].length === 4) { startDate = new Date(parseInt(parts[1]), parseInt(parts[0])-1, 1); } else if (parts.length === 3) { startDate = new Date(parseInt(parts[2]), parseInt(parts[1])-1, 1); } else { alert('Bitte Startdatum im Format MM.JJJJ eingeben, z.B. 01.2025'); return; } var start = startDate.getFullYear() + '-' + String(startDate.getMonth()+1).padStart(2,'0') + '-01'; var endDate = new Date(startDate.getFullYear(), startDate.getMonth() + anzahl, 1); var endStr = endDate.getFullYear() + '-' + String(endDate.getMonth()+1).padStart(2,'0') + '-01'; var rate = parseEuro(document.getElementById('rk-rate').value) || gesamt/anzahl; var row = { name: name, kategorie: document.getElementById('rk-cat').value, gesamt: gesamt, anzahl: anzahl, rate: rate, start_datum: start, end_datum: endStr, notiz: document.getElementById('rk-notiz').value.trim() }; sync('Speichere…'); try { if (rkEditId) { await db.from('ratenkaufe').update(row).eq('id', rkEditId); var i = ratenkaufe.findIndex(r=>r.id===rkEditId); if (i>=0) ratenkaufe[i] = Object.assign({}, ratenkaufe[i], row); } else { var res = await db.from('ratenkaufe').insert([row]).select(); if (res.data) ratenkaufe.unshift(res.data[0]); else { row.id = Date.now().toString(); ratenkaufe.unshift(row); } } } catch(e) { row.id = Date.now().toString(); ratenkaufe.unshift(row); } localStorage.setItem('mb_rk', JSON.stringify(ratenkaufe)); hideRatenkaufForm(); renderRatenkaufe(); unsync(); } async function deleteRatenkauf(id) { if (!confirm('Ratenkauf löschen?')) return; sync('Lösche…'); try { await db.from('ratenkaufe').delete().eq('id', id); } catch(e){} ratenkaufe = ratenkaufe.filter(r=>String(r.id)!==String(id)); localStorage.setItem('mb_rk', JSON.stringify(ratenkaufe)); renderRatenkaufe(); unsync(); } function renderRatenkaufe() { var now = new Date(); var aktiv = ratenkaufe.filter(r => new Date(r.end_datum) > now); var abgeschlossen = ratenkaufe.filter(r => new Date(r.end_datum) <= now); // Summary var totalMonatlich = aktiv.reduce((s,r)=>s+parseFloat(r.rate||0),0); var summaryEl = document.getElementById('rk-summary'); if (aktiv.length > 0) { summaryEl.innerHTML = `
MONATLICHE BELASTUNG
${fmt(totalMonatlich)}
${aktiv.length} aktive Rate${aktiv.length!==1?'n':''}
`; } else { summaryEl.innerHTML = ''; } var listEl = document.getElementById('rk-list'); if (!ratenkaufe.length) { listEl.innerHTML = '
Noch keine Ratenkäufe erfasst
'; return; } function renderCard(r, isAktiv) { var startDate = new Date(r.start_datum); var endDate = new Date(r.end_datum); var totalMonths = r.anzahl; var nowDate = new Date(); var elapsed = Math.max(0, (nowDate.getFullYear()-startDate.getFullYear())*12 + (nowDate.getMonth()-startDate.getMonth())); var remaining = Math.max(0, totalMonths - elapsed); var pct = Math.min(100, Math.round((elapsed/totalMonths)*100)); var bezahlt = Math.min(elapsed, totalMonths) * parseFloat(r.rate); var rest = parseFloat(r.gesamt) - bezahlt; var endLabel = endDate.toLocaleDateString('de-DE',{month:'long',year:'numeric'}); return `
${r.name}
${r.kategorie}${r.notiz?' · '+r.notiz:''}
${isAktiv?`aktiv`:`fertig`}
Gesamt
${fmt(parseFloat(r.gesamt))}
Rate/Monat
${fmt(parseFloat(r.rate))}
${isAktiv?'Noch':'Ende'}
${isAktiv?remaining+' Monate':endLabel}
Bezahlt: ${fmt(bezahlt)} ${pct}% Ende: ${endLabel}
`; } var html = ''; if (aktiv.length) { html += '
Laufend
'; html += aktiv.map(r=>renderCard(r,true)).join(''); } if (abgeschlossen.length) { html += '
Abgeschlossen
'; html += abgeschlossen.map(r=>renderCard(r,false)).join(''); } listEl.innerHTML = html; } function go(view) { const map={dash:"v-dash",add:"v-add",list:"v-list",darl:"v-darl",invest:"v-invest",raten:"v-raten",budget:"v-budget"}; const nmap={dash:"n-dash",add:"n-add",list:"n-list",darl:"n-darl",invest:"n-invest",raten:"n-raten",budget:"n-budget"}; Object.values(map).forEach(id=>document.getElementById(id).classList.remove("active")); Object.values(nmap).forEach(id=>document.getElementById(id).classList.remove("active")); document.getElementById(map[view]).classList.add("active"); document.getElementById(nmap[view]).classList.add("active"); if (view==="add") { editId=null; document.getElementById("form-title").textContent="Neue Buchung"; document.getElementById("save-btn").textContent="Speichern"; document.getElementById("datum").value=today(); document.getElementById("amount").value=""; document.getElementById("note").value=""; setType("out"); } if (view==="list") renderList(); if (view==="invest") { renderInvest(); fetchLivePrices(); } if (view==="raten") renderRatenkaufe(); if (view==="budget") { renderBudgetZiele(); renderDashBudget(); } } // ── INIT ───────────────────────────────────────────────── buildMonthSelect(); setType("out"); loadEntries(); loadDarlehen(); loadInvest(); loadRatenkaufe(); loadBudgetZiele(); fetchUsdEurRate(); +pnl.toFixed(2)+' (+'+roiPct+'%) · function setMvCurrency(cur) { mvCurrency = cur; var eurBtn = document.getElementById('iv-mv-eur'); var usdBtn = document.getElementById('iv-mv-usd'); var hint = document.getElementById('iv-mv-hint'); if (cur === 'EUR') { eurBtn.style.background = 'var(--blue)'; eurBtn.style.color = '#fff'; eurBtn.style.border = 'none'; usdBtn.style.background = 'var(--card2)'; usdBtn.style.color = '#aaa'; usdBtn.style.border = '1px solid var(--border)'; if (hint) hint.style.display = 'none'; } else { usdBtn.style.background = 'var(--blue)'; usdBtn.style.color = '#fff'; usdBtn.style.border = 'none'; eurBtn.style.background = 'var(--card2)'; eurBtn.style.color = '#aaa'; eurBtn.style.border = '1px solid var(--border)'; if (hint) { hint.style.display = 'block'; hint.textContent = '⏳ USD/EUR Kurs wird beim Speichern automatisch geladen…'; } } } async function saveInvestPos() { var name = document.getElementById('iv-name').value.trim(); var menge = parseFloat(document.getElementById('iv-menge').value.toString().replace(',','.')); var kaufpreis = getKaufpreisInEur(); if (!name || isNaN(menge) || isNaN(kaufpreis)) { alert('Bitte Name, Menge und Kaufpreis eingeben'); return; } // Handle USD -> EUR conversion for manual_val var rawMv = parseFloat(document.getElementById('iv-manual-val').value)||0; var manualVal = rawMv; if (mvCurrency === 'USD' && rawMv > 0) { var hint = document.getElementById('iv-mv-hint'); if (hint) hint.textContent = '⏳ USD/EUR Kurs wird geladen…'; var rate = usdEurRate || await fetchUsdEurRate(); manualVal = Math.round(rawMv * rate * 100) / 100; if (hint) hint.textContent = rawMv+' USD × '+rate.toFixed(4)+' = '+manualVal.toFixed(2)+' EUR ✓'; } var row = { name: name, typ: ivTyp, ticker: document.getElementById('iv-ticker').value.trim().toLowerCase(), depot: document.getElementById('iv-depot').value.trim(), menge: menge, kaufpreis: kaufpreis, manual_val: manualVal, notiz: document.getElementById('iv-notiz').value.trim() }; sync('Speichere…'); if (ivEditId) { await db.from('invest').update(row).eq('id', ivEditId); var i = investPositions.findIndex(function(p){ return p.id===ivEditId; }); if (i>=0) investPositions[i] = Object.assign({}, investPositions[i], row); } else { var res = await db.from('invest').insert([row]).select(); if (res.data) investPositions.unshift(res.data[0]); } closeInvestForm(); renderInvest(); fetchLivePrices(); unsync(); } async function deleteInvestPos(id) { if (!confirm('Position löschen?')) return; sync('Lösche…'); await db.from('invest').delete().eq('id', id); investPositions = investPositions.filter(function(p){ return p.id!==id; }); renderInvest(); renderDashInvest(); unsync(); } async function fetchLivePrices() { var cryptoTickers = []; var stockTickers = []; investPositions.forEach(function(p) { if ((p.typ==='crypto') && p.ticker) cryptoTickers.push(p.ticker); if ((p.typ==='stock'||p.typ==='etf') && p.ticker) stockTickers.push(p.ticker); }); // Fetch Crypto via CoinGecko with proxy fallbacks if (cryptoTickers.length > 0) { var ids = [...new Set(cryptoTickers)].join(','); var cgUrl = 'https://api.coingecko.com/api/v3/simple/price?ids='+ids+'&vs_currencies=eur'; var cgProxies = [ cgUrl, // direkt zuerst versuchen 'https://api.allorigins.win/raw?url='+encodeURIComponent(cgUrl), 'https://corsproxy.io/?'+encodeURIComponent(cgUrl), 'https://api.codetabs.com/v1/proxy?quest='+encodeURIComponent(cgUrl) ]; var cryptoFetched = false; for (var ci=0; ci 0) { Object.keys(priceData).forEach(function(k) { if (priceData[k] && priceData[k].eur) livePrices[k] = priceData[k].eur; }); cryptoFetched = true; console.log('Crypto fetched via proxy '+ci); } } catch(e) { console.log('Crypto proxy '+ci+' failed:', e.message); } } if (!cryptoFetched) console.log('All crypto proxies failed'); } // Fetch Stocks via Yahoo Finance with multiple proxies var uniqueStocks = [...new Set(stockTickers)]; for (var i=0; i 0) { if (currency === 'USD') price = price * 0.92; livePrices[sym.toLowerCase()] = price; fetched = true; console.log('Stock '+sym+' fetched: '+price+' via proxy'+pi+' url'+ui); } } catch(e) { console.log('Stock proxy'+pi+'/url'+ui+' failed for '+sym+':', e.message); } } } if (!fetched) console.log('All stock proxies failed for '+sym); } lastPriceUpdate = new Date(); renderInvest(); renderDashInvest(); } function getPrice(pos) { if (pos.typ === 'manual') return pos.manual_val || 0; var key = (pos.ticker || '').toLowerCase(); if (key && livePrices[key] !== undefined) return livePrices[key]; return null; // unknown } function renderInvest() { var depots = ['all']; investPositions.forEach(function(p) { if (p.depot && depots.indexOf(p.depot) === -1) depots.push(p.depot); }); // Depot filter buttons var filterEl = document.getElementById('iv-depot-filter'); if (filterEl) { filterEl.innerHTML = depots.map(function(d) { var active = d === ivDepotFilter; return ''; }).join(''); } var filtered = ivDepotFilter==='all' ? investPositions : investPositions.filter(function(p){ return p.depot===ivDepotFilter; }); var totalInvested = 0, totalCurrent = 0, unknownCount = 0; filtered.forEach(function(p) { var invested = (parseFloat(p.menge)||0) * (parseFloat(p.kaufpreis)||0); totalInvested += invested; var price = getPrice(p); if (price !== null) { totalCurrent += (parseFloat(p.menge)||0) * price; } else { unknownCount++; } }); var pnl = totalCurrent - totalInvested; var pnlPct = totalInvested > 0 ? (pnl/totalInvested*100) : 0; var pnlColor = pnl >= 0 ? 'var(--green)' : 'var(--red)'; var ivInv = document.getElementById('iv-invested'); var ivCur = document.getElementById('iv-current'); var ivPnl = document.getElementById('iv-pnl'); var ivPct = document.getElementById('iv-pnl-pct'); if (ivInv) ivInv.textContent = fmt(totalInvested); if (ivCur) ivCur.textContent = unknownCount>0 ? fmt(totalCurrent)+' *' : fmt(totalCurrent); if (ivPnl) { ivPnl.textContent = (pnl>=0?'+':'')+fmt(pnl); ivPnl.style.color=pnlColor; } if (ivPct) { ivPct.textContent = (pnlPct>=0?'+':'')+pnlPct.toFixed(2)+'%'; ivPct.style.color=pnlColor; } var listEl = document.getElementById('iv-list'); if (!listEl) return; if (filtered.length === 0) { listEl.innerHTML = '
Noch keine Positionen. Füge deine erste Position hinzu!
'; } else { var typIcons = {crypto:'₿', stock:'📊', etf:'📦', manual:'✍️'}; var typLabels = {crypto:'Crypto', stock:'Aktie', etf:'ETF', manual:'Manuell'}; listEl.innerHTML = filtered.map(function(p) { var invested = (parseFloat(p.menge)||0) * (parseFloat(p.kaufpreis)||0); var price = getPrice(p); var currentVal = price !== null ? (parseFloat(p.menge)||0) * price : null; var pnlPos = currentVal !== null ? currentVal - invested : null; var pnlPctPos = invested > 0 && pnlPos !== null ? (pnlPos/invested*100) : null; var pnlColor2 = pnlPos !== null && pnlPos >= 0 ? 'var(--green)' : 'var(--red)'; var priceStr = price !== null ? fmt(price) : 'wird geladen…'; var pnlStr = pnlPos !== null ? ''+(pnlPos>=0?'+':'')+fmt(pnlPos)+' '+(pnlPctPos>=0?'+':'')+pnlPctPos.toFixed(2)+'%' : ''; var html = ''; html += '
'; html += '
'; html += '
'; html += '
'+p.name+'
'; html += '
'+(typIcons[p.typ]||'')+' '+typLabels[p.typ]+(p.depot?' · '+p.depot:'')+(p.ticker?' · '+p.ticker.toUpperCase():'')+'
'; html += '
'; html += '
'; html += ''; html += ''; html += '
'; html += '
'; html += '
Menge
'+parseFloat(p.menge)+'
'; html += '
Ø Kauf
'+fmt(parseFloat(p.kaufpreis))+'
'; html += '
Kurs live
'+priceStr+'
'; html += '
'; html += '
'; // Row 1: Investiert vs Aktuell var currentVal = price !== null ? price * parseFloat(p.menge) : (p.typ === 'manual' ? parseFloat(p.manual_val||0) : null); html += '
'; html += '
Investiert: '+fmt(invested)+'
'; if (currentVal !== null) { html += '
Aktuell: '+fmt(currentVal)+'
'; } else { html += '
Aktuell: wird geladen…
'; } html += '
'; // Row 2: P&L html += '
'+pnlStr+'
'; html += '
'; // Notiz-Zeile html += '
'; html += '
'+(p.notiz ? p.notiz : 'Notiz hinzufügen…')+'
'; html += ''; html += '
'; // Manuelle Positionen: Quick-Wert-Update mit USD/EUR Toggle if (p.typ === "manual") { html += '
'; html += '
'; html += 'Akt. Wert:'; html += ''; html += ''; html += ''; html += '
'; html += ''; html += '
'; } html += '
'; return html; }).join(''); } var infoEl = document.getElementById('iv-update-info'); if (infoEl) { if (lastPriceUpdate) { var msg = 'Kurse: '+lastPriceUpdate.toLocaleTimeString('de-DE'); if (unknownCount > 0) msg += ' · '+unknownCount+' manuell'; infoEl.textContent = msg; infoEl.style.color = 'var(--green)'; } else { infoEl.textContent = 'Kurse konnten nicht geladen werden'; infoEl.style.color = 'var(--red)'; } } } async function ivRefreshPrices() { var btn = document.getElementById('iv-refresh-btn'); var info = document.getElementById('iv-update-info'); if (btn) { btn.textContent = '⏳ Laden…'; btn.disabled = true; } if (info) info.textContent = 'Kurse werden geladen…'; await fetchLivePrices(); if (btn) { btn.textContent = '↻ Aktualisieren'; btn.disabled = false; } } function setIvDepot(d) { ivDepotFilter = d; renderInvest(); } // Event delegation for invest list edit/delete buttons document.addEventListener('click', function(e) { var editId = e.target.getAttribute('data-edit-id'); var delId = e.target.getAttribute('data-del-id'); var qvId = e.target.getAttribute('data-qv-id'); var notizId = e.target.getAttribute('data-notiz-id'); var bzEdit = e.target.getAttribute('data-bz-edit'); var bzDel = e.target.getAttribute('data-bz-del'); if (editId) { openInvestForm(parseInt(editId)); } if (delId) { deleteInvestPos(parseInt(delId)); } if (qvId) { saveQuickVal(parseInt(qvId)); } if (notizId) { openNotizEdit(parseInt(notizId)); } if (bzEdit) { showBudgetForm(bzEdit); } if (bzDel) { deleteBudgetZiel(bzDel); } var qvCurId = e.target.getAttribute('data-qv-cur-id'); if (qvCurId) { toggleQvCurrency(qvCurId); } }); // USD/EUR state per position var qvCurrency = {}; // id -> 'EUR' or 'USD' var usdEurRate = null; async function fetchUsdEurRate() { try { var urls = [ 'https://api.allorigins.win/raw?url='+encodeURIComponent('https://api.frankfurter.app/latest?from=USD&to=EUR'), 'https://corsproxy.io/?'+encodeURIComponent('https://api.frankfurter.app/latest?from=USD&to=EUR'), 'https://api.frankfurter.app/latest?from=USD&to=EUR' ]; for (var i=0; i -1 && s.indexOf('.') > -1) { // Both: assume de-DE format: 2.855,46 s = s.replace(/\./g,'').replace(',','.'); } else { s = s.replace(',','.'); } return parseFloat(s) || 0; } // ── KATEGORIE MODAL ────────────────────────────────────── function openCatModal(type) { const f = filtered().filter(e => e.type === type); const cats = type === 'in' ? CATS_IN : CATS_OUT; const label = type === 'in' ? 'Einnahmen' : 'Ausgaben'; const total = f.reduce((s,e) => s + parseFloat(e.amount), 0); // Group by category const map = {}; f.forEach(e => { if (!map[e.category]) map[e.category] = []; map[e.category].push(e); }); // Sort categories by total descending const sorted = Object.entries(map).sort((a,b) => { const sa = a[1].reduce((s,e)=>s+parseFloat(e.amount),0); const sb = b[1].reduce((s,e)=>s+parseFloat(e.amount),0); return sb - sa; }); document.getElementById('cat-modal-title').textContent = label + ' · ' + MONTHS[curMonth] + ' ' + curYear; document.getElementById('cat-modal-sub').textContent = sorted.length + ' Kategorien · Gesamt: ' + fmt(total); let html = ''; if (!sorted.length) { html = '
Keine Einträge diesen Monat
'; } else { sorted.forEach(([cat, items]) => { const catTotal = items.reduce((s,e)=>s+parseFloat(e.amount),0); const pct = total > 0 ? (catTotal/total*100).toFixed(1) : 0; const color = cats.find(c=>c.l===cat)?.c || '#aaa'; const sortedItems = [...items].sort((a,b)=>new Date(b.date)-new Date(a.date)); html += `
${cat} ${pct}%
${type==='in'?'+':'-'}${fmt(catTotal)}
${sortedItems.map(e=>`
${e.note || '–'}
${e.date}
${fmt(parseFloat(e.amount))}
`).join('')}
`; }); } document.getElementById('cat-modal-body').innerHTML = html; const overlay = document.getElementById('cat-overlay'); overlay.style.display = 'flex'; } function closeCatModal(evt) { if (!evt || evt.target === document.getElementById('cat-overlay')) { document.getElementById('cat-overlay').style.display = 'none'; } } // ── BUDGET ZIELE ───────────────────────────────────────── var budgetZiele = []; var budgetEditId = null; async function loadBudgetZiele() { var res = await db.from('budgetziele').select('*').order('created_at', {ascending:true}); if (res.data) budgetZiele = res.data; renderBudgetZiele(); renderDashBudget(); } function showBudgetForm(id) { budgetEditId = id || null; var cats = CATS_OUT.map(function(c) { return ''; }).join(''); document.getElementById('budget-cat').innerHTML = cats; if (id) { var z = budgetZiele.find(function(x){ return x.id === id; }); if (z) { document.getElementById('budget-cat').value = z.kategorie; document.getElementById('budget-betrag').value = z.betrag; document.getElementById('budget-form-title').textContent = 'Ziel bearbeiten'; } } else { document.getElementById('budget-betrag').value = ''; document.getElementById('budget-form-title').textContent = 'Neues Budget-Ziel'; } document.getElementById('budget-form').style.display = 'block'; document.getElementById('budget-form').scrollIntoView({behavior:'smooth'}); } function hideBudgetForm() { document.getElementById('budget-form').style.display = 'none'; budgetEditId = null; } async function saveBudgetZiel() { var kat = document.getElementById('budget-cat').value; var betrag = parseFloat(document.getElementById('budget-betrag').value.toString().replace(',','.')); if (!kat || !betrag || betrag <= 0) { alert('Bitte Kategorie und Betrag angeben'); return; } var row = { kategorie: kat, betrag: betrag, typ: 'out' }; sync('Speichere…'); if (budgetEditId) { await db.from('budgetziele').update(row).eq('id', budgetEditId); var i = budgetZiele.findIndex(function(x){ return x.id === budgetEditId; }); if (i >= 0) budgetZiele[i] = Object.assign({}, budgetZiele[i], row); } else { var res = await db.from('budgetziele').insert([row]).select(); if (res.data) budgetZiele.push(res.data[0]); } hideBudgetForm(); renderBudgetZiele(); renderDashBudget(); unsync(); } async function deleteBudgetZiel(id) { if (!confirm('Budget-Ziel löschen?')) return; sync('Lösche…'); await db.from('budgetziele').delete().eq('id', id); budgetZiele = budgetZiele.filter(function(x){ return x.id !== id; }); renderBudgetZiele(); renderDashBudget(); unsync(); } function getBudgetSpend() { // Get current month spend per category var f = entries.filter(function(e) { var d = new Date(e.date); return e.type === 'out' && d.getMonth() === curMonth && d.getFullYear() === curYear; }); var spend = {}; f.forEach(function(e) { var cat = e.category || 'Sonstiges'; spend[cat] = (spend[cat] || 0) + parseFloat(e.amount); }); return spend; } function renderBudgetZiele() { var el = document.getElementById('budget-list'); if (!el) return; if (!budgetZiele.length) { el.innerHTML = '
Noch keine Budget-Ziele. Tippe auf "+ Neu" um eines anzulegen.
'; return; } var spend = getBudgetSpend(); var html = ''; budgetZiele.forEach(function(z) { var cat = z.kategorie; var limit = parseFloat(z.betrag); var used = spend[cat] || 0; var pct = Math.min(100, (used / limit * 100)); var over = used > limit; var color = over ? 'var(--red)' : pct > 80 ? '#f59e0b' : 'var(--green)'; var catColor = (CATS_OUT.find(function(c){ return c.l===cat; }) || {}).c || '#aaa'; html += '
'; html += '
'; html += '
'; html += '
'; html += ''+cat+''; html += '
'; html += '
'; html += ''; html += ''; html += '
'; html += '
'; html += 'Verbraucht'; html += ''+used.toLocaleString('de-DE',{minimumFractionDigits:2,maximumFractionDigits:2})+' €'; html += ' / '+limit.toLocaleString('de-DE',{minimumFractionDigits:2,maximumFractionDigits:2})+' €'; html += '
'; html += '
'; html += '
'; html += '
'; html += '
'; if (over) { html += '⚠️ '+(used-limit).toLocaleString('de-DE',{minimumFractionDigits:2,maximumFractionDigits:2})+' € überzogen'; } else { html += ''+(limit-used).toLocaleString('de-DE',{minimumFractionDigits:2,maximumFractionDigits:2})+' € noch verfügbar'; } html += ''+pct.toFixed(0)+'%'; html += '
'; }); el.innerHTML = html; } function renderDashBudget() { var el = document.getElementById('dash-budget-summary'); if (!el) return; if (!budgetZiele.length) { el.innerHTML = '
Noch keine Ziele definiert
'; return; } var spend = getBudgetSpend(); var html = ''; budgetZiele.forEach(function(z) { var cat = z.kategorie; var limit = parseFloat(z.betrag); var used = spend[cat] || 0; var pct = Math.min(100, (used / limit * 100)); var over = used > limit; var color = over ? 'var(--red)' : pct > 80 ? '#f59e0b' : 'var(--green)'; var catColor = (CATS_OUT.find(function(c){ return c.l===cat; }) || {}).c || '#aaa'; html += '
'; html += '
'; html += '
'; html += '
'; html += ''+cat+''; html += '
'; html += ''; html += used.toLocaleString('de-DE',{minimumFractionDigits:2,maximumFractionDigits:2})+' / '+limit.toLocaleString('de-DE',{minimumFractionDigits:2,maximumFractionDigits:2})+' €'; html += '
'; html += '
'; html += '
'; html += '
'; if (over) html += '
⚠️ '+(used-limit).toLocaleString('de-DE',{minimumFractionDigits:2,maximumFractionDigits:2})+' € überzogen
'; html += '
'; }); el.innerHTML = html; } // ── RATENKÄUFE ─────────────────────────────────────────── var ratenkaufe = []; var rkEditId = null; async function loadRatenkaufe() { try { var res = await db.from('ratenkaufe').select('*').order('created_at', {ascending:false}); if (res.data) ratenkaufe = res.data; } catch(e) { try { ratenkaufe = JSON.parse(localStorage.getItem('mb_rk')||'[]'); } catch(e2){ ratenkaufe=[]; } } renderRatenkaufe(); renderDashRatenkaufe(); } function editRatenkauf(id) { var r = ratenkaufe.find(function(x){ return String(x.id) === String(id); }); if (!r) return; rkEditId = id; document.getElementById('rk-form-title').textContent = 'Ratenkauf bearbeiten'; document.getElementById('rk-name').value = r.name || ''; document.getElementById('rk-cat').value = r.kategorie || 'Hardware'; document.getElementById('rk-gesamt').value = parseFloat(r.gesamt).toLocaleString('de-DE', {minimumFractionDigits:2, maximumFractionDigits:2}); document.getElementById('rk-anzahl').value = r.anzahl || ''; document.getElementById('rk-rate').value = parseFloat(r.rate).toLocaleString('de-DE', {minimumFractionDigits:2, maximumFractionDigits:2}); document.getElementById('rk-notiz').value = r.notiz || ''; // Format start date as MM.JJJJ if (r.start_datum) { var d = new Date(r.start_datum); document.getElementById('rk-start').value = String(d.getMonth()+1).padStart(2,'0') + '.' + d.getFullYear(); } calcRkRate(); document.getElementById('rk-form').style.display = 'block'; document.getElementById('rk-form').scrollIntoView({behavior:'smooth'}); } function showRatenkaufForm() { rkEditId = null; document.getElementById('rk-form-title').textContent = 'Neuer Ratenkauf'; document.getElementById('rk-name').value = ''; document.getElementById('rk-gesamt').value = ''; document.getElementById('rk-anzahl').value = ''; document.getElementById('rk-rate').value = ''; document.getElementById('rk-notiz').value = ''; document.getElementById('rk-start').value = '01.' + new Date().getFullYear(); document.getElementById('rk-form').style.display = 'block'; document.getElementById('rk-form').scrollIntoView({behavior:'smooth'}); } function calcRkRate() { var g = parseEuro(document.getElementById('rk-gesamt').value); var n = parseInt(document.getElementById('rk-anzahl').value||0); var startRaw = document.getElementById('rk-start').value.trim(); if (g > 0 && n > 0) { document.getElementById('rk-rate').value = (g/n).toLocaleString('de-DE',{minimumFractionDigits:2,maximumFractionDigits:2}); } // Show letzte Rate preview var letzteRow = document.getElementById('rk-letzte-row'); if (g > 0 && n > 0 && startRaw.length >= 6) { var parts = startRaw.split('.'); var startDate; if (parts.length === 2 && parts[1].length === 4) { startDate = new Date(parseInt(parts[1]), parseInt(parts[0])-1, 1); } else if (parts.length === 3 && parts[2].length === 4) { startDate = new Date(parseInt(parts[2]), parseInt(parts[1])-1, 1); } if (startDate && !isNaN(startDate)) { var endDate = new Date(startDate.getFullYear(), startDate.getMonth() + n - 1, 1); var now = new Date(); var elapsed = Math.max(0, (now.getFullYear()-startDate.getFullYear())*12 + (now.getMonth()-startDate.getMonth())); var bezahltRaten = Math.min(elapsed, n); var bezahltBetrag = bezahltRaten * (g/n); document.getElementById('rk-letzte-label').textContent = endDate.toLocaleDateString('de-DE',{month:'long',year:'numeric'}); document.getElementById('rk-bezahlt-label').textContent = bezahltRaten + ' von ' + n + ' Raten · ' + bezahltBetrag.toLocaleString('de-DE',{minimumFractionDigits:2,maximumFractionDigits:2}) + ' €'; letzteRow.style.display = 'block'; } else { letzteRow.style.display = 'none'; } } else { letzteRow.style.display = 'none'; } } function hideRatenkaufForm() { document.getElementById('rk-form').style.display = 'none'; rkEditId = null; } async function saveRatenkauf() { var name = document.getElementById('rk-name').value.trim(); var gesamt = parseEuro(document.getElementById('rk-gesamt').value); var anzahl = parseInt(document.getElementById('rk-anzahl').value||0); var startRaw = document.getElementById('rk-start').value.trim(); if (!name || !gesamt || !anzahl || !startRaw) { alert('Bitte alle Pflichtfelder ausfüllen'); return; } // Parse MM.JJJJ oder TT.MM.JJJJ var parts = startRaw.split('.'); var startDate; if (parts.length === 2 && parts[1].length === 4) { startDate = new Date(parseInt(parts[1]), parseInt(parts[0])-1, 1); } else if (parts.length === 3) { startDate = new Date(parseInt(parts[2]), parseInt(parts[1])-1, 1); } else { alert('Bitte Startdatum im Format MM.JJJJ eingeben, z.B. 01.2025'); return; } var start = startDate.getFullYear() + '-' + String(startDate.getMonth()+1).padStart(2,'0') + '-01'; var endDate = new Date(startDate.getFullYear(), startDate.getMonth() + anzahl, 1); var endStr = endDate.getFullYear() + '-' + String(endDate.getMonth()+1).padStart(2,'0') + '-01'; var rate = parseEuro(document.getElementById('rk-rate').value) || gesamt/anzahl; var row = { name: name, kategorie: document.getElementById('rk-cat').value, gesamt: gesamt, anzahl: anzahl, rate: rate, start_datum: start, end_datum: endStr, notiz: document.getElementById('rk-notiz').value.trim() }; sync('Speichere…'); try { if (rkEditId) { await db.from('ratenkaufe').update(row).eq('id', rkEditId); var i = ratenkaufe.findIndex(r=>r.id===rkEditId); if (i>=0) ratenkaufe[i] = Object.assign({}, ratenkaufe[i], row); } else { var res = await db.from('ratenkaufe').insert([row]).select(); if (res.data) ratenkaufe.unshift(res.data[0]); else { row.id = Date.now().toString(); ratenkaufe.unshift(row); } } } catch(e) { row.id = Date.now().toString(); ratenkaufe.unshift(row); } localStorage.setItem('mb_rk', JSON.stringify(ratenkaufe)); hideRatenkaufForm(); renderRatenkaufe(); unsync(); } async function deleteRatenkauf(id) { if (!confirm('Ratenkauf löschen?')) return; sync('Lösche…'); try { await db.from('ratenkaufe').delete().eq('id', id); } catch(e){} ratenkaufe = ratenkaufe.filter(r=>String(r.id)!==String(id)); localStorage.setItem('mb_rk', JSON.stringify(ratenkaufe)); renderRatenkaufe(); unsync(); } function renderRatenkaufe() { var now = new Date(); var aktiv = ratenkaufe.filter(r => new Date(r.end_datum) > now); var abgeschlossen = ratenkaufe.filter(r => new Date(r.end_datum) <= now); // Summary var totalMonatlich = aktiv.reduce((s,r)=>s+parseFloat(r.rate||0),0); var summaryEl = document.getElementById('rk-summary'); if (aktiv.length > 0) { summaryEl.innerHTML = `
MONATLICHE BELASTUNG
${fmt(totalMonatlich)}
${aktiv.length} aktive Rate${aktiv.length!==1?'n':''}
`; } else { summaryEl.innerHTML = ''; } var listEl = document.getElementById('rk-list'); if (!ratenkaufe.length) { listEl.innerHTML = '
Noch keine Ratenkäufe erfasst
'; return; } function renderCard(r, isAktiv) { var startDate = new Date(r.start_datum); var endDate = new Date(r.end_datum); var totalMonths = r.anzahl; var nowDate = new Date(); var elapsed = Math.max(0, (nowDate.getFullYear()-startDate.getFullYear())*12 + (nowDate.getMonth()-startDate.getMonth())); var remaining = Math.max(0, totalMonths - elapsed); var pct = Math.min(100, Math.round((elapsed/totalMonths)*100)); var bezahlt = Math.min(elapsed, totalMonths) * parseFloat(r.rate); var rest = parseFloat(r.gesamt) - bezahlt; var endLabel = endDate.toLocaleDateString('de-DE',{month:'long',year:'numeric'}); return `
${r.name}
${r.kategorie}${r.notiz?' · '+r.notiz:''}
${isAktiv?`aktiv`:`fertig`}
Gesamt
${fmt(parseFloat(r.gesamt))}
Rate/Monat
${fmt(parseFloat(r.rate))}
${isAktiv?'Noch':'Ende'}
${isAktiv?remaining+' Monate':endLabel}
Bezahlt: ${fmt(bezahlt)} ${pct}% Ende: ${endLabel}
`; } var html = ''; if (aktiv.length) { html += '
Laufend
'; html += aktiv.map(r=>renderCard(r,true)).join(''); } if (abgeschlossen.length) { html += '
Abgeschlossen
'; html += abgeschlossen.map(r=>renderCard(r,false)).join(''); } listEl.innerHTML = html; } function go(view) { const map={dash:"v-dash",add:"v-add",list:"v-list",darl:"v-darl",invest:"v-invest",raten:"v-raten",budget:"v-budget"}; const nmap={dash:"n-dash",add:"n-add",list:"n-list",darl:"n-darl",invest:"n-invest",raten:"n-raten",budget:"n-budget"}; Object.values(map).forEach(id=>document.getElementById(id).classList.remove("active")); Object.values(nmap).forEach(id=>document.getElementById(id).classList.remove("active")); document.getElementById(map[view]).classList.add("active"); document.getElementById(nmap[view]).classList.add("active"); if (view==="add") { editId=null; document.getElementById("form-title").textContent="Neue Buchung"; document.getElementById("save-btn").textContent="Speichern"; document.getElementById("datum").value=today(); document.getElementById("amount").value=""; document.getElementById("note").value=""; setType("out"); } if (view==="list") renderList(); if (view==="invest") { renderInvest(); fetchLivePrices(); } if (view==="raten") renderRatenkaufe(); if (view==="budget") { renderBudgetZiele(); renderDashBudget(); } } // ── INIT ───────────────────────────────────────────────── buildMonthSelect(); setType("out"); loadEntries(); loadDarlehen(); loadInvest(); loadRatenkaufe(); loadBudgetZiele(); fetchUsdEurRate(); +real.toFixed(2)+' real investiert ( durch Reinvestition ) '+today; } resEl.innerHTML = html; resEl.style.display = 'block'; } var mvCurrency = 'EUR'; // currency for manual value in edit modal function setMvCurrency(cur) { mvCurrency = cur; var eurBtn = document.getElementById('iv-mv-eur'); var usdBtn = document.getElementById('iv-mv-usd'); var hint = document.getElementById('iv-mv-hint'); if (cur === 'EUR') { eurBtn.style.background = 'var(--blue)'; eurBtn.style.color = '#fff'; eurBtn.style.border = 'none'; usdBtn.style.background = 'var(--card2)'; usdBtn.style.color = '#aaa'; usdBtn.style.border = '1px solid var(--border)'; if (hint) hint.style.display = 'none'; } else { usdBtn.style.background = 'var(--blue)'; usdBtn.style.color = '#fff'; usdBtn.style.border = 'none'; eurBtn.style.background = 'var(--card2)'; eurBtn.style.color = '#aaa'; eurBtn.style.border = '1px solid var(--border)'; if (hint) { hint.style.display = 'block'; hint.textContent = '⏳ USD/EUR Kurs wird beim Speichern automatisch geladen…'; } } } async function saveInvestPos() { var name = document.getElementById('iv-name').value.trim(); var menge = parseFloat(document.getElementById('iv-menge').value.toString().replace(',','.')); var kaufpreis = getKaufpreisInEur(); if (!name || isNaN(menge) || isNaN(kaufpreis)) { alert('Bitte Name, Menge und Kaufpreis eingeben'); return; } // Handle USD -> EUR conversion for manual_val var rawMv = parseFloat(document.getElementById('iv-manual-val').value)||0; var manualVal = rawMv; if (mvCurrency === 'USD' && rawMv > 0) { var hint = document.getElementById('iv-mv-hint'); if (hint) hint.textContent = '⏳ USD/EUR Kurs wird geladen…'; var rate = usdEurRate || await fetchUsdEurRate(); manualVal = Math.round(rawMv * rate * 100) / 100; if (hint) hint.textContent = rawMv+' USD × '+rate.toFixed(4)+' = '+manualVal.toFixed(2)+' EUR ✓'; } var row = { name: name, typ: ivTyp, ticker: document.getElementById('iv-ticker').value.trim().toLowerCase(), depot: document.getElementById('iv-depot').value.trim(), menge: menge, kaufpreis: kaufpreis, manual_val: manualVal, notiz: document.getElementById('iv-notiz').value.trim() }; sync('Speichere…'); if (ivEditId) { await db.from('invest').update(row).eq('id', ivEditId); var i = investPositions.findIndex(function(p){ return p.id===ivEditId; }); if (i>=0) investPositions[i] = Object.assign({}, investPositions[i], row); } else { var res = await db.from('invest').insert([row]).select(); if (res.data) investPositions.unshift(res.data[0]); } closeInvestForm(); renderInvest(); fetchLivePrices(); unsync(); } async function deleteInvestPos(id) { if (!confirm('Position löschen?')) return; sync('Lösche…'); await db.from('invest').delete().eq('id', id); investPositions = investPositions.filter(function(p){ return p.id!==id; }); renderInvest(); renderDashInvest(); unsync(); } async function fetchLivePrices() { var cryptoTickers = []; var stockTickers = []; investPositions.forEach(function(p) { if ((p.typ==='crypto') && p.ticker) cryptoTickers.push(p.ticker); if ((p.typ==='stock'||p.typ==='etf') && p.ticker) stockTickers.push(p.ticker); }); // Fetch Crypto via CoinGecko with proxy fallbacks if (cryptoTickers.length > 0) { var ids = [...new Set(cryptoTickers)].join(','); var cgUrl = 'https://api.coingecko.com/api/v3/simple/price?ids='+ids+'&vs_currencies=eur'; var cgProxies = [ cgUrl, // direkt zuerst versuchen 'https://api.allorigins.win/raw?url='+encodeURIComponent(cgUrl), 'https://corsproxy.io/?'+encodeURIComponent(cgUrl), 'https://api.codetabs.com/v1/proxy?quest='+encodeURIComponent(cgUrl) ]; var cryptoFetched = false; for (var ci=0; ci 0) { Object.keys(priceData).forEach(function(k) { if (priceData[k] && priceData[k].eur) livePrices[k] = priceData[k].eur; }); cryptoFetched = true; console.log('Crypto fetched via proxy '+ci); } } catch(e) { console.log('Crypto proxy '+ci+' failed:', e.message); } } if (!cryptoFetched) console.log('All crypto proxies failed'); } // Fetch Stocks via Yahoo Finance with multiple proxies var uniqueStocks = [...new Set(stockTickers)]; for (var i=0; i 0) { if (currency === 'USD') price = price * 0.92; livePrices[sym.toLowerCase()] = price; fetched = true; console.log('Stock '+sym+' fetched: '+price+' via proxy'+pi+' url'+ui); } } catch(e) { console.log('Stock proxy'+pi+'/url'+ui+' failed for '+sym+':', e.message); } } } if (!fetched) console.log('All stock proxies failed for '+sym); } lastPriceUpdate = new Date(); renderInvest(); renderDashInvest(); } function getPrice(pos) { if (pos.typ === 'manual') return pos.manual_val || 0; var key = (pos.ticker || '').toLowerCase(); if (key && livePrices[key] !== undefined) return livePrices[key]; return null; // unknown } function renderInvest() { var depots = ['all']; investPositions.forEach(function(p) { if (p.depot && depots.indexOf(p.depot) === -1) depots.push(p.depot); }); // Depot filter buttons var filterEl = document.getElementById('iv-depot-filter'); if (filterEl) { filterEl.innerHTML = depots.map(function(d) { var active = d === ivDepotFilter; return ''; }).join(''); } var filtered = ivDepotFilter==='all' ? investPositions : investPositions.filter(function(p){ return p.depot===ivDepotFilter; }); var totalInvested = 0, totalCurrent = 0, unknownCount = 0; filtered.forEach(function(p) { var invested = (parseFloat(p.menge)||0) * (parseFloat(p.kaufpreis)||0); totalInvested += invested; var price = getPrice(p); if (price !== null) { totalCurrent += (parseFloat(p.menge)||0) * price; } else { unknownCount++; } }); var pnl = totalCurrent - totalInvested; var pnlPct = totalInvested > 0 ? (pnl/totalInvested*100) : 0; var pnlColor = pnl >= 0 ? 'var(--green)' : 'var(--red)'; var ivInv = document.getElementById('iv-invested'); var ivCur = document.getElementById('iv-current'); var ivPnl = document.getElementById('iv-pnl'); var ivPct = document.getElementById('iv-pnl-pct'); if (ivInv) ivInv.textContent = fmt(totalInvested); if (ivCur) ivCur.textContent = unknownCount>0 ? fmt(totalCurrent)+' *' : fmt(totalCurrent); if (ivPnl) { ivPnl.textContent = (pnl>=0?'+':'')+fmt(pnl); ivPnl.style.color=pnlColor; } if (ivPct) { ivPct.textContent = (pnlPct>=0?'+':'')+pnlPct.toFixed(2)+'%'; ivPct.style.color=pnlColor; } var listEl = document.getElementById('iv-list'); if (!listEl) return; if (filtered.length === 0) { listEl.innerHTML = '
Noch keine Positionen. Füge deine erste Position hinzu!
'; } else { var typIcons = {crypto:'₿', stock:'📊', etf:'📦', manual:'✍️'}; var typLabels = {crypto:'Crypto', stock:'Aktie', etf:'ETF', manual:'Manuell'}; listEl.innerHTML = filtered.map(function(p) { var invested = (parseFloat(p.menge)||0) * (parseFloat(p.kaufpreis)||0); var price = getPrice(p); var currentVal = price !== null ? (parseFloat(p.menge)||0) * price : null; var pnlPos = currentVal !== null ? currentVal - invested : null; var pnlPctPos = invested > 0 && pnlPos !== null ? (pnlPos/invested*100) : null; var pnlColor2 = pnlPos !== null && pnlPos >= 0 ? 'var(--green)' : 'var(--red)'; var priceStr = price !== null ? fmt(price) : 'wird geladen…'; var pnlStr = pnlPos !== null ? ''+(pnlPos>=0?'+':'')+fmt(pnlPos)+' '+(pnlPctPos>=0?'+':'')+pnlPctPos.toFixed(2)+'%' : ''; var html = ''; html += '
'; html += '
'; html += '
'; html += '
'+p.name+'
'; html += '
'+(typIcons[p.typ]||'')+' '+typLabels[p.typ]+(p.depot?' · '+p.depot:'')+(p.ticker?' · '+p.ticker.toUpperCase():'')+'
'; html += '
'; html += '
'; html += ''; html += ''; html += '
'; html += '
'; html += '
Menge
'+parseFloat(p.menge)+'
'; html += '
Ø Kauf
'+fmt(parseFloat(p.kaufpreis))+'
'; html += '
Kurs live
'+priceStr+'
'; html += '
'; html += '
'; // Row 1: Investiert vs Aktuell var currentVal = price !== null ? price * parseFloat(p.menge) : (p.typ === 'manual' ? parseFloat(p.manual_val||0) : null); html += '
'; html += '
Investiert: '+fmt(invested)+'
'; if (currentVal !== null) { html += '
Aktuell: '+fmt(currentVal)+'
'; } else { html += '
Aktuell: wird geladen…
'; } html += '
'; // Row 2: P&L html += '
'+pnlStr+'
'; html += '
'; // Notiz-Zeile html += '
'; html += '
'+(p.notiz ? p.notiz : 'Notiz hinzufügen…')+'
'; html += ''; html += '
'; // Manuelle Positionen: Quick-Wert-Update mit USD/EUR Toggle if (p.typ === "manual") { html += '
'; html += '
'; html += 'Akt. Wert:'; html += ''; html += ''; html += ''; html += '
'; html += ''; html += '
'; } html += '
'; return html; }).join(''); } var infoEl = document.getElementById('iv-update-info'); if (infoEl) { if (lastPriceUpdate) { var msg = 'Kurse: '+lastPriceUpdate.toLocaleTimeString('de-DE'); if (unknownCount > 0) msg += ' · '+unknownCount+' manuell'; infoEl.textContent = msg; infoEl.style.color = 'var(--green)'; } else { infoEl.textContent = 'Kurse konnten nicht geladen werden'; infoEl.style.color = 'var(--red)'; } } } async function ivRefreshPrices() { var btn = document.getElementById('iv-refresh-btn'); var info = document.getElementById('iv-update-info'); if (btn) { btn.textContent = '⏳ Laden…'; btn.disabled = true; } if (info) info.textContent = 'Kurse werden geladen…'; await fetchLivePrices(); if (btn) { btn.textContent = '↻ Aktualisieren'; btn.disabled = false; } } function setIvDepot(d) { ivDepotFilter = d; renderInvest(); } // Event delegation for invest list edit/delete buttons document.addEventListener('click', function(e) { var editId = e.target.getAttribute('data-edit-id'); var delId = e.target.getAttribute('data-del-id'); var qvId = e.target.getAttribute('data-qv-id'); var notizId = e.target.getAttribute('data-notiz-id'); var bzEdit = e.target.getAttribute('data-bz-edit'); var bzDel = e.target.getAttribute('data-bz-del'); if (editId) { openInvestForm(parseInt(editId)); } if (delId) { deleteInvestPos(parseInt(delId)); } if (qvId) { saveQuickVal(parseInt(qvId)); } if (notizId) { openNotizEdit(parseInt(notizId)); } if (bzEdit) { showBudgetForm(bzEdit); } if (bzDel) { deleteBudgetZiel(bzDel); } var qvCurId = e.target.getAttribute('data-qv-cur-id'); if (qvCurId) { toggleQvCurrency(qvCurId); } }); // USD/EUR state per position var qvCurrency = {}; // id -> 'EUR' or 'USD' var usdEurRate = null; async function fetchUsdEurRate() { try { var urls = [ 'https://api.allorigins.win/raw?url='+encodeURIComponent('https://api.frankfurter.app/latest?from=USD&to=EUR'), 'https://corsproxy.io/?'+encodeURIComponent('https://api.frankfurter.app/latest?from=USD&to=EUR'), 'https://api.frankfurter.app/latest?from=USD&to=EUR' ]; for (var i=0; i -1 && s.indexOf('.') > -1) { // Both: assume de-DE format: 2.855,46 s = s.replace(/\./g,'').replace(',','.'); } else { s = s.replace(',','.'); } return parseFloat(s) || 0; } // ── KATEGORIE MODAL ────────────────────────────────────── function openCatModal(type) { const f = filtered().filter(e => e.type === type); const cats = type === 'in' ? CATS_IN : CATS_OUT; const label = type === 'in' ? 'Einnahmen' : 'Ausgaben'; const total = f.reduce((s,e) => s + parseFloat(e.amount), 0); // Group by category const map = {}; f.forEach(e => { if (!map[e.category]) map[e.category] = []; map[e.category].push(e); }); // Sort categories by total descending const sorted = Object.entries(map).sort((a,b) => { const sa = a[1].reduce((s,e)=>s+parseFloat(e.amount),0); const sb = b[1].reduce((s,e)=>s+parseFloat(e.amount),0); return sb - sa; }); document.getElementById('cat-modal-title').textContent = label + ' · ' + MONTHS[curMonth] + ' ' + curYear; document.getElementById('cat-modal-sub').textContent = sorted.length + ' Kategorien · Gesamt: ' + fmt(total); let html = ''; if (!sorted.length) { html = '
Keine Einträge diesen Monat
'; } else { sorted.forEach(([cat, items]) => { const catTotal = items.reduce((s,e)=>s+parseFloat(e.amount),0); const pct = total > 0 ? (catTotal/total*100).toFixed(1) : 0; const color = cats.find(c=>c.l===cat)?.c || '#aaa'; const sortedItems = [...items].sort((a,b)=>new Date(b.date)-new Date(a.date)); html += `
${cat} ${pct}%
${type==='in'?'+':'-'}${fmt(catTotal)}
${sortedItems.map(e=>`
${e.note || '–'}
${e.date}
${fmt(parseFloat(e.amount))}
`).join('')}
`; }); } document.getElementById('cat-modal-body').innerHTML = html; const overlay = document.getElementById('cat-overlay'); overlay.style.display = 'flex'; } function closeCatModal(evt) { if (!evt || evt.target === document.getElementById('cat-overlay')) { document.getElementById('cat-overlay').style.display = 'none'; } } // ── BUDGET ZIELE ───────────────────────────────────────── var budgetZiele = []; var budgetEditId = null; async function loadBudgetZiele() { var res = await db.from('budgetziele').select('*').order('created_at', {ascending:true}); if (res.data) budgetZiele = res.data; renderBudgetZiele(); renderDashBudget(); } function showBudgetForm(id) { budgetEditId = id || null; var cats = CATS_OUT.map(function(c) { return ''; }).join(''); document.getElementById('budget-cat').innerHTML = cats; if (id) { var z = budgetZiele.find(function(x){ return x.id === id; }); if (z) { document.getElementById('budget-cat').value = z.kategorie; document.getElementById('budget-betrag').value = z.betrag; document.getElementById('budget-form-title').textContent = 'Ziel bearbeiten'; } } else { document.getElementById('budget-betrag').value = ''; document.getElementById('budget-form-title').textContent = 'Neues Budget-Ziel'; } document.getElementById('budget-form').style.display = 'block'; document.getElementById('budget-form').scrollIntoView({behavior:'smooth'}); } function hideBudgetForm() { document.getElementById('budget-form').style.display = 'none'; budgetEditId = null; } async function saveBudgetZiel() { var kat = document.getElementById('budget-cat').value; var betrag = parseFloat(document.getElementById('budget-betrag').value.toString().replace(',','.')); if (!kat || !betrag || betrag <= 0) { alert('Bitte Kategorie und Betrag angeben'); return; } var row = { kategorie: kat, betrag: betrag, typ: 'out' }; sync('Speichere…'); if (budgetEditId) { await db.from('budgetziele').update(row).eq('id', budgetEditId); var i = budgetZiele.findIndex(function(x){ return x.id === budgetEditId; }); if (i >= 0) budgetZiele[i] = Object.assign({}, budgetZiele[i], row); } else { var res = await db.from('budgetziele').insert([row]).select(); if (res.data) budgetZiele.push(res.data[0]); } hideBudgetForm(); renderBudgetZiele(); renderDashBudget(); unsync(); } async function deleteBudgetZiel(id) { if (!confirm('Budget-Ziel löschen?')) return; sync('Lösche…'); await db.from('budgetziele').delete().eq('id', id); budgetZiele = budgetZiele.filter(function(x){ return x.id !== id; }); renderBudgetZiele(); renderDashBudget(); unsync(); } function getBudgetSpend() { // Get current month spend per category var f = entries.filter(function(e) { var d = new Date(e.date); return e.type === 'out' && d.getMonth() === curMonth && d.getFullYear() === curYear; }); var spend = {}; f.forEach(function(e) { var cat = e.category || 'Sonstiges'; spend[cat] = (spend[cat] || 0) + parseFloat(e.amount); }); return spend; } function renderBudgetZiele() { var el = document.getElementById('budget-list'); if (!el) return; if (!budgetZiele.length) { el.innerHTML = '
Noch keine Budget-Ziele. Tippe auf "+ Neu" um eines anzulegen.
'; return; } var spend = getBudgetSpend(); var html = ''; budgetZiele.forEach(function(z) { var cat = z.kategorie; var limit = parseFloat(z.betrag); var used = spend[cat] || 0; var pct = Math.min(100, (used / limit * 100)); var over = used > limit; var color = over ? 'var(--red)' : pct > 80 ? '#f59e0b' : 'var(--green)'; var catColor = (CATS_OUT.find(function(c){ return c.l===cat; }) || {}).c || '#aaa'; html += '
'; html += '
'; html += '
'; html += '
'; html += ''+cat+''; html += '
'; html += '
'; html += ''; html += ''; html += '
'; html += '
'; html += 'Verbraucht'; html += ''+used.toLocaleString('de-DE',{minimumFractionDigits:2,maximumFractionDigits:2})+' €'; html += ' / '+limit.toLocaleString('de-DE',{minimumFractionDigits:2,maximumFractionDigits:2})+' €'; html += '
'; html += '
'; html += '
'; html += '
'; html += '
'; if (over) { html += '⚠️ '+(used-limit).toLocaleString('de-DE',{minimumFractionDigits:2,maximumFractionDigits:2})+' € überzogen'; } else { html += ''+(limit-used).toLocaleString('de-DE',{minimumFractionDigits:2,maximumFractionDigits:2})+' € noch verfügbar'; } html += ''+pct.toFixed(0)+'%'; html += '
'; }); el.innerHTML = html; } function renderDashBudget() { var el = document.getElementById('dash-budget-summary'); if (!el) return; if (!budgetZiele.length) { el.innerHTML = '
Noch keine Ziele definiert
'; return; } var spend = getBudgetSpend(); var html = ''; budgetZiele.forEach(function(z) { var cat = z.kategorie; var limit = parseFloat(z.betrag); var used = spend[cat] || 0; var pct = Math.min(100, (used / limit * 100)); var over = used > limit; var color = over ? 'var(--red)' : pct > 80 ? '#f59e0b' : 'var(--green)'; var catColor = (CATS_OUT.find(function(c){ return c.l===cat; }) || {}).c || '#aaa'; html += '
'; html += '
'; html += '
'; html += '
'; html += ''+cat+''; html += '
'; html += ''; html += used.toLocaleString('de-DE',{minimumFractionDigits:2,maximumFractionDigits:2})+' / '+limit.toLocaleString('de-DE',{minimumFractionDigits:2,maximumFractionDigits:2})+' €'; html += '
'; html += '
'; html += '
'; html += '
'; if (over) html += '
⚠️ '+(used-limit).toLocaleString('de-DE',{minimumFractionDigits:2,maximumFractionDigits:2})+' € überzogen
'; html += '
'; }); el.innerHTML = html; } // ── RATENKÄUFE ─────────────────────────────────────────── var ratenkaufe = []; var rkEditId = null; async function loadRatenkaufe() { try { var res = await db.from('ratenkaufe').select('*').order('created_at', {ascending:false}); if (res.data) ratenkaufe = res.data; } catch(e) { try { ratenkaufe = JSON.parse(localStorage.getItem('mb_rk')||'[]'); } catch(e2){ ratenkaufe=[]; } } renderRatenkaufe(); renderDashRatenkaufe(); } function editRatenkauf(id) { var r = ratenkaufe.find(function(x){ return String(x.id) === String(id); }); if (!r) return; rkEditId = id; document.getElementById('rk-form-title').textContent = 'Ratenkauf bearbeiten'; document.getElementById('rk-name').value = r.name || ''; document.getElementById('rk-cat').value = r.kategorie || 'Hardware'; document.getElementById('rk-gesamt').value = parseFloat(r.gesamt).toLocaleString('de-DE', {minimumFractionDigits:2, maximumFractionDigits:2}); document.getElementById('rk-anzahl').value = r.anzahl || ''; document.getElementById('rk-rate').value = parseFloat(r.rate).toLocaleString('de-DE', {minimumFractionDigits:2, maximumFractionDigits:2}); document.getElementById('rk-notiz').value = r.notiz || ''; // Format start date as MM.JJJJ if (r.start_datum) { var d = new Date(r.start_datum); document.getElementById('rk-start').value = String(d.getMonth()+1).padStart(2,'0') + '.' + d.getFullYear(); } calcRkRate(); document.getElementById('rk-form').style.display = 'block'; document.getElementById('rk-form').scrollIntoView({behavior:'smooth'}); } function showRatenkaufForm() { rkEditId = null; document.getElementById('rk-form-title').textContent = 'Neuer Ratenkauf'; document.getElementById('rk-name').value = ''; document.getElementById('rk-gesamt').value = ''; document.getElementById('rk-anzahl').value = ''; document.getElementById('rk-rate').value = ''; document.getElementById('rk-notiz').value = ''; document.getElementById('rk-start').value = '01.' + new Date().getFullYear(); document.getElementById('rk-form').style.display = 'block'; document.getElementById('rk-form').scrollIntoView({behavior:'smooth'}); } function calcRkRate() { var g = parseEuro(document.getElementById('rk-gesamt').value); var n = parseInt(document.getElementById('rk-anzahl').value||0); var startRaw = document.getElementById('rk-start').value.trim(); if (g > 0 && n > 0) { document.getElementById('rk-rate').value = (g/n).toLocaleString('de-DE',{minimumFractionDigits:2,maximumFractionDigits:2}); } // Show letzte Rate preview var letzteRow = document.getElementById('rk-letzte-row'); if (g > 0 && n > 0 && startRaw.length >= 6) { var parts = startRaw.split('.'); var startDate; if (parts.length === 2 && parts[1].length === 4) { startDate = new Date(parseInt(parts[1]), parseInt(parts[0])-1, 1); } else if (parts.length === 3 && parts[2].length === 4) { startDate = new Date(parseInt(parts[2]), parseInt(parts[1])-1, 1); } if (startDate && !isNaN(startDate)) { var endDate = new Date(startDate.getFullYear(), startDate.getMonth() + n - 1, 1); var now = new Date(); var elapsed = Math.max(0, (now.getFullYear()-startDate.getFullYear())*12 + (now.getMonth()-startDate.getMonth())); var bezahltRaten = Math.min(elapsed, n); var bezahltBetrag = bezahltRaten * (g/n); document.getElementById('rk-letzte-label').textContent = endDate.toLocaleDateString('de-DE',{month:'long',year:'numeric'}); document.getElementById('rk-bezahlt-label').textContent = bezahltRaten + ' von ' + n + ' Raten · ' + bezahltBetrag.toLocaleString('de-DE',{minimumFractionDigits:2,maximumFractionDigits:2}) + ' €'; letzteRow.style.display = 'block'; } else { letzteRow.style.display = 'none'; } } else { letzteRow.style.display = 'none'; } } function hideRatenkaufForm() { document.getElementById('rk-form').style.display = 'none'; rkEditId = null; } async function saveRatenkauf() { var name = document.getElementById('rk-name').value.trim(); var gesamt = parseEuro(document.getElementById('rk-gesamt').value); var anzahl = parseInt(document.getElementById('rk-anzahl').value||0); var startRaw = document.getElementById('rk-start').value.trim(); if (!name || !gesamt || !anzahl || !startRaw) { alert('Bitte alle Pflichtfelder ausfüllen'); return; } // Parse MM.JJJJ oder TT.MM.JJJJ var parts = startRaw.split('.'); var startDate; if (parts.length === 2 && parts[1].length === 4) { startDate = new Date(parseInt(parts[1]), parseInt(parts[0])-1, 1); } else if (parts.length === 3) { startDate = new Date(parseInt(parts[2]), parseInt(parts[1])-1, 1); } else { alert('Bitte Startdatum im Format MM.JJJJ eingeben, z.B. 01.2025'); return; } var start = startDate.getFullYear() + '-' + String(startDate.getMonth()+1).padStart(2,'0') + '-01'; var endDate = new Date(startDate.getFullYear(), startDate.getMonth() + anzahl, 1); var endStr = endDate.getFullYear() + '-' + String(endDate.getMonth()+1).padStart(2,'0') + '-01'; var rate = parseEuro(document.getElementById('rk-rate').value) || gesamt/anzahl; var row = { name: name, kategorie: document.getElementById('rk-cat').value, gesamt: gesamt, anzahl: anzahl, rate: rate, start_datum: start, end_datum: endStr, notiz: document.getElementById('rk-notiz').value.trim() }; sync('Speichere…'); try { if (rkEditId) { await db.from('ratenkaufe').update(row).eq('id', rkEditId); var i = ratenkaufe.findIndex(r=>r.id===rkEditId); if (i>=0) ratenkaufe[i] = Object.assign({}, ratenkaufe[i], row); } else { var res = await db.from('ratenkaufe').insert([row]).select(); if (res.data) ratenkaufe.unshift(res.data[0]); else { row.id = Date.now().toString(); ratenkaufe.unshift(row); } } } catch(e) { row.id = Date.now().toString(); ratenkaufe.unshift(row); } localStorage.setItem('mb_rk', JSON.stringify(ratenkaufe)); hideRatenkaufForm(); renderRatenkaufe(); unsync(); } async function deleteRatenkauf(id) { if (!confirm('Ratenkauf löschen?')) return; sync('Lösche…'); try { await db.from('ratenkaufe').delete().eq('id', id); } catch(e){} ratenkaufe = ratenkaufe.filter(r=>String(r.id)!==String(id)); localStorage.setItem('mb_rk', JSON.stringify(ratenkaufe)); renderRatenkaufe(); unsync(); } function renderRatenkaufe() { var now = new Date(); var aktiv = ratenkaufe.filter(r => new Date(r.end_datum) > now); var abgeschlossen = ratenkaufe.filter(r => new Date(r.end_datum) <= now); // Summary var totalMonatlich = aktiv.reduce((s,r)=>s+parseFloat(r.rate||0),0); var summaryEl = document.getElementById('rk-summary'); if (aktiv.length > 0) { summaryEl.innerHTML = `
MONATLICHE BELASTUNG
${fmt(totalMonatlich)}
${aktiv.length} aktive Rate${aktiv.length!==1?'n':''}
`; } else { summaryEl.innerHTML = ''; } var listEl = document.getElementById('rk-list'); if (!ratenkaufe.length) { listEl.innerHTML = '
Noch keine Ratenkäufe erfasst
'; return; } function renderCard(r, isAktiv) { var startDate = new Date(r.start_datum); var endDate = new Date(r.end_datum); var totalMonths = r.anzahl; var nowDate = new Date(); var elapsed = Math.max(0, (nowDate.getFullYear()-startDate.getFullYear())*12 + (nowDate.getMonth()-startDate.getMonth())); var remaining = Math.max(0, totalMonths - elapsed); var pct = Math.min(100, Math.round((elapsed/totalMonths)*100)); var bezahlt = Math.min(elapsed, totalMonths) * parseFloat(r.rate); var rest = parseFloat(r.gesamt) - bezahlt; var endLabel = endDate.toLocaleDateString('de-DE',{month:'long',year:'numeric'}); return `
${r.name}
${r.kategorie}${r.notiz?' · '+r.notiz:''}
${isAktiv?`aktiv`:`fertig`}
Gesamt
${fmt(parseFloat(r.gesamt))}
Rate/Monat
${fmt(parseFloat(r.rate))}
${isAktiv?'Noch':'Ende'}
${isAktiv?remaining+' Monate':endLabel}
Bezahlt: ${fmt(bezahlt)} ${pct}% Ende: ${endLabel}
`; } var html = ''; if (aktiv.length) { html += '
Laufend
'; html += aktiv.map(r=>renderCard(r,true)).join(''); } if (abgeschlossen.length) { html += '
Abgeschlossen
'; html += abgeschlossen.map(r=>renderCard(r,false)).join(''); } listEl.innerHTML = html; } function go(view) { const map={dash:"v-dash",add:"v-add",list:"v-list",darl:"v-darl",invest:"v-invest",raten:"v-raten",budget:"v-budget"}; const nmap={dash:"n-dash",add:"n-add",list:"n-list",darl:"n-darl",invest:"n-invest",raten:"n-raten",budget:"n-budget"}; Object.values(map).forEach(id=>document.getElementById(id).classList.remove("active")); Object.values(nmap).forEach(id=>document.getElementById(id).classList.remove("active")); document.getElementById(map[view]).classList.add("active"); document.getElementById(nmap[view]).classList.add("active"); if (view==="add") { editId=null; document.getElementById("form-title").textContent="Neue Buchung"; document.getElementById("save-btn").textContent="Speichern"; document.getElementById("datum").value=today(); document.getElementById("amount").value=""; document.getElementById("note").value=""; setType("out"); } if (view==="list") renderList(); if (view==="invest") { renderInvest(); fetchLivePrices(); } if (view==="raten") renderRatenkaufe(); if (view==="budget") { renderBudgetZiele(); renderDashBudget(); } } // ── INIT ───────────────────────────────────────────────── buildMonthSelect(); setType("out"); loadEntries(); loadDarlehen(); loadInvest(); loadRatenkaufe(); loadBudgetZiele(); fetchUsdEurRate();