Spaces:
Running
Running
| <html lang="de"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Haushaltsbuch Pro - Ausgaben-Tracker</title> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
| <style> | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| } | |
| :root { | |
| --primary: #6366f1; | |
| --primary-dark: #4f46e5; | |
| --secondary: #22d3ee; | |
| --success: #10b981; | |
| --danger: #ef4444; | |
| --warning: #f59e0b; | |
| --dark: #1e293b; | |
| --light: #f8fafc; | |
| --gray: #64748b; | |
| --border: #e2e8f0; | |
| --shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1); | |
| --shadow-lg: 0 10px 25px -5px rgb(0 0 0 / 0.1); | |
| } | |
| body { | |
| font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif; | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| min-height: 100vh; | |
| color: var(--dark); | |
| } | |
| .container { | |
| max-width: 1400px; | |
| margin: 0 auto; | |
| padding: 20px; | |
| } | |
| header { | |
| background: rgba(255, 255, 255, 0.98); | |
| backdrop-filter: blur(10px); | |
| border-radius: 20px; | |
| padding: 25px; | |
| margin-bottom: 30px; | |
| box-shadow: var(--shadow-lg); | |
| animation: slideDown 0.5s ease; | |
| } | |
| .header-content { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| flex-wrap: wrap; | |
| gap: 20px; | |
| } | |
| .logo { | |
| display: flex; | |
| align-items: center; | |
| gap: 15px; | |
| font-size: 28px; | |
| font-weight: 700; | |
| color: var(--primary); | |
| } | |
| .logo i { | |
| font-size: 36px; | |
| background: linear-gradient(135deg, var(--primary), var(--secondary)); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| } | |
| .header-stats { | |
| display: flex; | |
| gap: 30px; | |
| flex-wrap: wrap; | |
| } | |
| .stat-item { | |
| text-align: center; | |
| } | |
| .stat-value { | |
| font-size: 24px; | |
| font-weight: 700; | |
| color: var(--dark); | |
| } | |
| .stat-label { | |
| font-size: 12px; | |
| color: var(--gray); | |
| text-transform: uppercase; | |
| letter-spacing: 1px; | |
| } | |
| .grid-container { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fit, minmax(350px, 1fr)); | |
| gap: 25px; | |
| margin-bottom: 30px; | |
| } | |
| .card { | |
| background: rgba(255, 255, 255, 0.98); | |
| backdrop-filter: blur(10px); | |
| border-radius: 20px; | |
| padding: 25px; | |
| box-shadow: var(--shadow-lg); | |
| animation: fadeInUp 0.5s ease; | |
| transition: transform 0.3s ease, box-shadow 0.3s ease; | |
| } | |
| .card:hover { | |
| transform: translateY(-5px); | |
| box-shadow: 0 20px 40px -10px rgb(0 0 0 / 0.2); | |
| } | |
| .card-header { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| margin-bottom: 20px; | |
| padding-bottom: 15px; | |
| border-bottom: 2px solid var(--border); | |
| } | |
| .card-title { | |
| font-size: 20px; | |
| font-weight: 600; | |
| color: var(--dark); | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| } | |
| .card-title i { | |
| color: var(--primary); | |
| } | |
| .form-group { | |
| margin-bottom: 20px; | |
| } | |
| label { | |
| display: block; | |
| margin-bottom: 8px; | |
| font-weight: 500; | |
| color: var(--dark); | |
| font-size: 14px; | |
| } | |
| input, select, textarea { | |
| width: 100%; | |
| padding: 12px 15px; | |
| border: 2px solid var(--border); | |
| border-radius: 10px; | |
| font-size: 15px; | |
| transition: all 0.3s ease; | |
| background: white; | |
| } | |
| input:focus, select:focus, textarea:focus { | |
| outline: none; | |
| border-color: var(--primary); | |
| box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1); | |
| } | |
| .btn { | |
| padding: 12px 24px; | |
| border: none; | |
| border-radius: 10px; | |
| font-size: 15px; | |
| font-weight: 600; | |
| cursor: pointer; | |
| transition: all 0.3s ease; | |
| display: inline-flex; | |
| align-items: center; | |
| gap: 8px; | |
| } | |
| .btn-primary { | |
| background: linear-gradient(135deg, var(--primary), var(--primary-dark)); | |
| color: white; | |
| } | |
| .btn-primary:hover { | |
| transform: translateY(-2px); | |
| box-shadow: 0 10px 20px -5px rgba(99, 102, 241, 0.4); | |
| } | |
| .btn-success { | |
| background: linear-gradient(135deg, var(--success), #059669); | |
| color: white; | |
| } | |
| .btn-danger { | |
| background: linear-gradient(135deg, var(--danger), #dc2626); | |
| color: white; | |
| } | |
| .btn-secondary { | |
| background: var(--light); | |
| color: var(--dark); | |
| border: 2px solid var(--border); | |
| } | |
| .btn-group { | |
| display: flex; | |
| gap: 10px; | |
| flex-wrap: wrap; | |
| } | |
| .transaction-list { | |
| max-height: 400px; | |
| overflow-y: auto; | |
| padding-right: 10px; | |
| } | |
| .transaction-item { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| padding: 15px; | |
| margin-bottom: 10px; | |
| background: var(--light); | |
| border-radius: 10px; | |
| transition: all 0.3s ease; | |
| border-left: 4px solid transparent; | |
| } | |
| .transaction-item:hover { | |
| transform: translateX(5px); | |
| box-shadow: var(--shadow); | |
| } | |
| .transaction-item.income { | |
| border-left-color: var(--success); | |
| } | |
| .transaction-item.expense { | |
| border-left-color: var(--danger); | |
| } | |
| .transaction-info { | |
| flex: 1; | |
| } | |
| .transaction-category { | |
| font-size: 12px; | |
| color: var(--gray); | |
| margin-top: 4px; | |
| } | |
| .transaction-amount { | |
| font-weight: 700; | |
| font-size: 18px; | |
| } | |
| .amount-income { | |
| color: var(--success); | |
| } | |
| .amount-expense { | |
| color: var(--danger); | |
| } | |
| .chart-container { | |
| position: relative; | |
| height: 300px; | |
| margin-top: 20px; | |
| } | |
| canvas { | |
| max-width: 100%; | |
| height: auto ; | |
| } | |
| .category-badge { | |
| display: inline-block; | |
| padding: 4px 12px; | |
| border-radius: 20px; | |
| font-size: 12px; | |
| font-weight: 600; | |
| margin-right: 8px; | |
| } | |
| .modal { | |
| display: none; | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| background: rgba(0, 0, 0, 0.5); | |
| backdrop-filter: blur(5px); | |
| z-index: 1000; | |
| animation: fadeIn 0.3s ease; | |
| } | |
| .modal.active { | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| } | |
| .modal-content { | |
| background: white; | |
| border-radius: 20px; | |
| padding: 30px; | |
| max-width: 500px; | |
| width: 90%; | |
| max-height: 90vh; | |
| overflow-y: auto; | |
| animation: slideUp 0.3s ease; | |
| } | |
| .toast { | |
| position: fixed; | |
| bottom: 30px; | |
| right: 30px; | |
| padding: 15px 20px; | |
| background: white; | |
| border-radius: 10px; | |
| box-shadow: var(--shadow-lg); | |
| display: none; | |
| align-items: center; | |
| gap: 10px; | |
| animation: slideInRight 0.3s ease; | |
| z-index: 2000; | |
| } | |
| .toast.show { | |
| display: flex; | |
| } | |
| .toast.success { | |
| border-left: 4px solid var(--success); | |
| } | |
| .toast.error { | |
| border-left: 4px solid var(--danger); | |
| } | |
| .empty-state { | |
| text-align: center; | |
| padding: 40px; | |
| color: var(--gray); | |
| } | |
| .empty-state i { | |
| font-size: 48px; | |
| margin-bottom: 15px; | |
| opacity: 0.5; | |
| } | |
| .filter-tabs { | |
| display: flex; | |
| gap: 10px; | |
| margin-bottom: 20px; | |
| flex-wrap: wrap; | |
| } | |
| .tab { | |
| padding: 8px 16px; | |
| background: var(--light); | |
| border: 2px solid var(--border); | |
| border-radius: 20px; | |
| cursor: pointer; | |
| transition: all 0.3s ease; | |
| font-size: 14px; | |
| font-weight: 500; | |
| } | |
| .tab.active { | |
| background: var(--primary); | |
| color: white; | |
| border-color: var(--primary); | |
| } | |
| .tab:hover:not(.active) { | |
| background: var(--border); | |
| } | |
| @keyframes fadeIn { | |
| from { opacity: 0; } | |
| to { opacity: 1; } | |
| } | |
| @keyframes fadeInUp { | |
| from { | |
| opacity: 0; | |
| transform: translateY(20px); | |
| } | |
| to { | |
| opacity: 1; | |
| transform: translateY(0); | |
| } | |
| } | |
| @keyframes slideDown { | |
| from { | |
| opacity: 0; | |
| transform: translateY(-20px); | |
| } | |
| to { | |
| opacity: 1; | |
| transform: translateY(0); | |
| } | |
| } | |
| @keyframes slideUp { | |
| from { | |
| opacity: 0; | |
| transform: translateY(20px); | |
| } | |
| to { | |
| opacity: 1; | |
| transform: translateY(0); | |
| } | |
| } | |
| @keyframes slideInRight { | |
| from { | |
| opacity: 0; | |
| transform: translateX(20px); | |
| } | |
| to { | |
| opacity: 1; | |
| transform: translateX(0); | |
| } | |
| } | |
| @media (max-width: 768px) { | |
| .container { | |
| padding: 10px; | |
| } | |
| .grid-container { | |
| grid-template-columns: 1fr; | |
| } | |
| .header-content { | |
| flex-direction: column; | |
| text-align: center; | |
| } | |
| .header-stats { | |
| justify-content: center; | |
| } | |
| .btn-group { | |
| flex-direction: column; | |
| } | |
| .btn { | |
| width: 100%; | |
| justify-content: center; | |
| } | |
| } | |
| .summary-cards { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); | |
| gap: 15px; | |
| margin-bottom: 25px; | |
| } | |
| .summary-card { | |
| background: linear-gradient(135deg, var(--light), white); | |
| padding: 20px; | |
| border-radius: 15px; | |
| text-align: center; | |
| border: 2px solid var(--border); | |
| transition: all 0.3s ease; | |
| } | |
| .summary-card:hover { | |
| transform: translateY(-3px); | |
| box-shadow: var(--shadow); | |
| } | |
| .summary-card-icon { | |
| font-size: 24px; | |
| margin-bottom: 10px; | |
| } | |
| .summary-card-value { | |
| font-size: 24px; | |
| font-weight: 700; | |
| margin-bottom: 5px; | |
| } | |
| .summary-card-label { | |
| font-size: 12px; | |
| color: var(--gray); | |
| text-transform: uppercase; | |
| } | |
| .built-with { | |
| position: absolute; | |
| top: 10px; | |
| right: 20px; | |
| font-size: 12px; | |
| color: var(--gray); | |
| } | |
| .built-with a { | |
| color: var(--primary); | |
| text-decoration: none; | |
| font-weight: 600; | |
| } | |
| .built-with a:hover { | |
| text-decoration: underline; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <header> | |
| <div class="built-with"> | |
| Built with <a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank">anycoder</a> | |
| </div> | |
| <div class="header-content"> | |
| <div class="logo"> | |
| <i class="fas fa-wallet"></i> | |
| <span>Haushaltsbuch Pro</span> | |
| </div> | |
| <div class="header-stats"> | |
| <div class="stat-item"> | |
| <div class="stat-value" id="totalBalance">0,00 €</div> | |
| <div class="stat-label">Gesamtguthaben</div> | |
| </div> | |
| <div class="stat-item"> | |
| <div class="stat-value" id="monthIncome">0,00 €</div> | |
| <div class="stat-label">Monatseinnahmen</div> | |
| </div> | |
| <div class="stat-item"> | |
| <div class="stat-value" id="monthExpense">0,00 €</div> | |
| <div class="stat-label">Monatsausgaben</div> | |
| </div> | |
| </div> | |
| </div> | |
| </header> | |
| <div class="summary-cards"> | |
| <div class="summary-card"> | |
| <div class="summary-card-icon" style="color: var(--success);"> | |
| <i class="fas fa-arrow-trend-up"></i> | |
| </div> | |
| <div class="summary-card-value" style="color: var(--success);" id="todayIncome">0,00 €</div> | |
| <div class="summary-card-label">Heutige Einnahmen</div> | |
| </div> | |
| <div class="summary-card"> | |
| <div class="summary-card-icon" style="color: var(--danger);"> | |
| <i class="fas fa-arrow-trend-down"></i> | |
| </div> | |
| <div class="summary-card-value" style="color: var(--danger);" id="todayExpense">0,00 €</div> | |
| <div class="summary-card-label">Heutige Ausgaben</div> | |
| </div> | |
| <div class="summary-card"> | |
| <div class="summary-card-icon" style="color: var(--primary);"> | |
| <i class="fas fa-chart-pie"></i> | |
| </div> | |
| <div class="summary-card-value" style="color: var(--primary);" id="categoryCount">0</div> | |
| <div class="summary-card-label">Kategorien</div> | |
| </div> | |
| <div class="summary-card"> | |
| <div class="summary-card-icon" style="color: var(--warning);"> | |
| <i class="fas fa-receipt"></i> | |
| </div> | |
| <div class="summary-card-value" style="color: var(--warning);" id="transactionCount">0</div> | |
| <div class="summary-card-label">Transaktionen</div> | |
| </div> | |
| </div> | |
| <div class="grid-container"> | |
| <div class="card"> | |
| <div class="card-header"> | |
| <h2 class="card-title"> | |
| <i class="fas fa-plus-circle"></i> | |
| Neue Buchung | |
| </h2> | |
| </div> | |
| <form id="transactionForm"> | |
| <div class="form-group"> | |
| <label for="type">Typ</label> | |
| <select id="type" required> | |
| <option value="">Bitte wählen</option> | |
| <option value="income">Einnahme</option> | |
| <option value="expense">Ausgabe</option> | |
| </select> | |
| </div> | |
| <div class="form-group"> | |
| <label for="amount">Betrag (€)</label> | |
| <input type="number" id="amount" step="0.01" min="0" required placeholder="0,00"> | |
| </div> | |
| <div class="form-group"> | |
| <label for="category">Kategorie</label> | |
| <select id="category" required> | |
| <option value="">Bitte wählen</option> | |
| </select> | |
| </div> | |
| <div class="form-group"> | |
| <label for="date">Datum</label> | |
| <input type="date" id="date" required> | |
| </div> | |
| <div class="form-group"> | |
| <label for="description">Beschreibung</label> | |
| <textarea id="description" rows="2" placeholder="Optionale Beschreibung..."></textarea> | |
| </div> | |
| <button type="submit" class="btn btn-primary" style="width: 100%;"> | |
| <i class="fas fa-save"></i> | |
| Buchung speichern | |
| </button> | |
| </form> | |
| </div> | |
| <div class="card"> | |
| <div class="card-header"> | |
| <h2 class="card-title"> | |
| <i class="fas fa-history"></i> | |
| Letzte Buchungen | |
| </h2> | |
| <button class="btn btn-secondary" onclick="clearAllTransactions()"> | |
| <i class="fas fa-trash"></i> | |
| </button> | |
| </div> | |
| <div class="filter-tabs"> | |
| <div class="tab active" onclick="filterTransactions('all')">Alle</div> | |
| <div class="tab" onclick="filterTransactions('income')">Einnahmen</div> | |
| <div class="tab" onclick="filterTransactions('expense')">Ausgaben</div> | |
| </div> | |
| <div class="transaction-list" id="transactionList"> | |
| <div class="empty-state"> | |
| <i class="fas fa-inbox"></i> | |
| <p>Keine Buchungen vorhanden</p> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="grid-container"> | |
| <div class="card"> | |
| <div class="card-header"> | |
| <h2 class="card-title"> | |
| <i class="fas fa-chart-line"></i> | |
| Ausgaben nach Kategorien | |
| </h2> | |
| </div> | |
| <div class="chart-container"> | |
| <canvas id="categoryChart"></canvas> | |
| </div> | |
| </div> | |
| <div class="card"> | |
| <div class="card-header"> | |
| <h2 class="card-title"> | |
| <i class="fas fa-calendar-alt"></i> | |
| Monatlicher Verlauf | |
| </h2> | |
| </div> | |
| <div class="chart-container"> | |
| <canvas id="monthlyChart"></canvas> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="card"> | |
| <div class="card-header"> | |
| <h2 class="card-title"> | |
| <i class="fas fa-cog"></i> | |
| Datenverwaltung | |
| </h2> | |
| </div> | |
| <div class="btn-group"> | |
| <button class="btn btn-success" onclick="exportToCSV()"> | |
| <i class="fas fa-download"></i> | |
| CSV exportieren | |
| </button> | |
| <label class="btn btn-primary"> | |
| <i class="fas fa-upload"></i> | |
| CSV importieren | |
| <input type="file" accept=".csv" style="display: none;" onchange="importFromCSV(event)"> | |
| </label> | |
| <button class="btn btn-danger" onclick="clearAllData()"> | |
| <i class="fas fa-trash-alt"></i> | |
| Alle Daten löschen | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="toast" id="toast"> | |
| <i class="fas fa-check-circle"></i> | |
| <span id="toastMessage">Erfolgreich gespeichert!</span> | |
| </div> | |
| <script> | |
| // Daten initialisieren | |
| let transactions = JSON.parse(localStorage.getItem('transactions')) || []; | |
| let currentFilter = 'all'; | |
| const categories = { | |
| income: ['Gehalt', 'Nebenjob', 'Investment', 'Geschenk', 'Sonstige Einnahmen'], | |
| expense: ['Lebensmittel', 'Miete', 'Transport', 'Utilities', 'Unterhaltung', 'Gesundheit', 'Shopping', 'Bildung', 'Restaurant', 'Sonstige Ausgaben'] | |
| }; | |
| // Datum auf heute setzen | |
| document.getElementById('date').valueAsDate = new Date(); | |
| // Typ-Änderung behandeln | |
| document.getElementById('type').addEventListener('change', function() { | |
| updateCategoryOptions(this.value); | |
| }); | |
| function updateCategoryOptions(type) { | |
| const categorySelect = document.getElementById('category'); | |
| categorySelect.innerHTML = '<option value="">Bitte wählen</option>'; | |
| if (type && categories[type]) { | |
| categories[type].forEach(cat => { | |
| const option = document.createElement('option'); | |
| option.value = cat; | |
| option.textContent = cat; | |
| categorySelect.appendChild(option); | |
| }); | |
| } | |
| } | |
| // Formular absenden | |
| document.getElementById('transactionForm').addEventListener('submit', function(e) { | |
| e.preventDefault(); | |
| const transaction = { | |
| id: Date.now(), | |
| type: document.getElementById('type').value, | |
| amount: parseFloat(document.getElementById('amount').value), | |
| category: document.getElementById('category').value, | |
| date: document.getElementById('date').value, | |
| description: document.getElementById('description').value, | |
| timestamp: new Date().toISOString() | |
| }; | |
| transactions.push(transaction); | |
| saveTransactions(); | |
| updateUI(); | |
| // Formular zurücksetzen | |
| this.reset(); | |
| document.getElementById('date').valueAsDate = new Date(); | |
| showToast('Buchung erfolgreich gespeichert!', 'success'); | |
| }); | |
| function saveTransactions() { | |
| localStorage.setItem('transactions', JSON.stringify(transactions)); | |
| } | |
| function updateUI() { | |
| updateStats(); | |
| updateTransactionList(); | |
| updateCharts(); | |
| } | |
| function updateStats() { | |
| const now = new Date(); | |
| const currentMonth = now.getMonth(); | |
| const currentYear = now.getFullYear(); | |
| const today = now.toISOString().split('T')[0]; | |
| let totalIncome = 0; | |
| let totalExpense = 0; | |
| let monthIncome = 0; | |
| let monthExpense = 0; | |
| let todayIncome = 0; | |
| let todayExpense = 0; | |
| let uniqueCategories = new Set(); | |
| transactions.forEach(t => { | |
| const tDate = new Date(t.date); | |
| const tMonth = tDate.getMonth(); | |
| const tYear = tDate.getFullYear(); | |
| uniqueCategories.add(t.category); | |
| if (t.type === 'income') { | |
| totalIncome += t.amount; | |
| if (tMonth === currentMonth && tYear === currentYear) { | |
| monthIncome += t.amount; | |
| } | |
| if (t.date === today) { | |
| todayIncome += t.amount; | |
| } | |
| } else { | |
| totalExpense += t.amount; | |
| if (tMonth === currentMonth && tYear === currentYear) { | |
| monthExpense += t.amount; | |
| } | |
| if (t.date === today) { | |
| todayExpense += t.amount; | |
| } | |
| } | |
| }); | |
| const balance = totalIncome - totalExpense; | |
| document.getElementById('totalBalance').textContent = formatCurrency(balance); | |
| document.getElementById('monthIncome').textContent = formatCurrency(monthIncome); | |
| document.getElementById('monthExpense').textContent = formatCurrency(monthExpense); | |
| document.getElementById('todayIncome').textContent = formatCurrency(todayIncome); | |
| document.getElementById('todayExpense').textContent = formatCurrency(todayExpense); | |
| document.getElementById('categoryCount').textContent = uniqueCategories.size; | |
| document.getElementById('transactionCount').textContent = transactions.length; | |
| } | |
| function updateTransactionList() { | |
| const list = document.getElementById('transactionList'); | |
| let filteredTransactions = transactions; | |
| if (currentFilter !== 'all') { | |
| filteredTransactions = transactions.filter(t => t.type === currentFilter); | |
| } | |
| // Sortieren nach Datum (neueste zuerst) | |
| filteredTransactions.sort((a, b) => new Date(b.date) - new Date(a.date)); | |
| if (filteredTransactions.length === 0) { | |
| list.innerHTML = ` | |
| <div class="empty-state"> | |
| <i class="fas fa-inbox"></i> | |
| <p>Keine Buchungen vorhanden</p> | |
| </div> | |
| `; | |
| return; | |
| } | |
| list.innerHTML = filteredTransactions.slice(0, 10).map(t => ` | |
| <div class="transaction-item ${t.type}"> | |
| <div class="transaction-info"> | |
| <div>${t.description || t.category}</div> | |
| <div class="transaction-category">${t.category} • ${formatDate(t.date)}</div> | |
| </div> | |
| <div class="transaction-amount amount-${t.type}"> | |
| ${t.type === 'income' ? '+' : '-'}${formatCurrency(t.amount)} | |
| </div> | |
| <button class="btn btn-danger" onclick="deleteTransaction(${t.id})" style="padding: 8px 12px;"> | |
| <i class="fas fa-times"></i> | |
| </button> | |
| </div> | |
| `).join(''); | |
| } | |
| function deleteTransaction(id) { | |
| if (confirm('Möchten Sie diese Buchung wirklich löschen?')) { | |
| transactions = transactions.filter(t => t.id !== id); | |
| saveTransactions(); | |
| updateUI(); | |
| showToast('Buchung gelöscht', 'success'); | |
| } | |
| } | |
| function filterTransactions(type) { | |
| currentFilter = type; | |
| // Tabs aktualisieren | |
| document.querySelectorAll('.tab').forEach(tab => { | |
| tab.classList.remove('active'); | |
| }); | |
| event.target.classList.add('active'); | |
| updateTransactionList(); | |
| } | |
| function updateCharts() { | |
| drawCategoryChart(); | |
| drawMonthlyChart(); | |
| } | |
| function drawCategoryChart() { | |
| const canvas = document.getElementById('categoryChart'); | |
| const ctx = canvas.getContext('2d'); | |
| // Kategorie-Ausgaben summieren | |
| const categoryTotals = {}; | |
| transactions.filter(t => t.type === 'expense').forEach(t => { | |
| categoryTotals[t.category] = (categoryTotals[t.category] || 0) + t.amount; | |
| }); | |
| const labels = Object.keys(categoryTotals); | |
| const data = Object.values(categoryTotals); | |
| if (labels.length === 0) { | |
| ctx.clearRect(0, 0, canvas.width, canvas.height); | |
| ctx.font = '16px Arial'; | |
| ctx.fillStyle = '#999'; | |
| ctx.textAlign = 'center'; | |
| ctx.fillText('Keine Daten verfügbar', canvas.width / 2, canvas.height / 2); | |
| return; | |
| } | |
| // Farben generieren | |
| const colors = labels.map((_, i) => `hsl(${(i * 360) / labels.length}, 70%, 60%)`); | |
| // Canvas-Größe setzen | |
| canvas.width = canvas.offsetWidth; | |
| canvas.height = 300; | |
| // Kuchendiagramm zeichnen | |
| const centerX = canvas.width / 2; | |
| const centerY = canvas.height / 2; | |
| const radius = Math.min(centerX, centerY) - 40; | |
| let currentAngle = -Math.PI / 2; | |
| const total = data.reduce((sum, val) => sum + val, 0); | |
| data.forEach((value, i) => { | |
| const sliceAngle = (value / total) * 2 * Math.PI; | |
| // Segment zeichnen | |
| ctx.beginPath(); | |
| ctx.arc(centerX, centerY, radius, currentAngle, currentAngle + sliceAngle); | |
| ctx.lineTo(centerX, centerY); | |
| ctx.fillStyle = colors[i]; | |
| ctx.fill(); | |
| // Label zeichnen | |
| const labelAngle = currentAngle + sliceAngle / 2; | |
| const labelX = centerX + Math.cos(labelAngle) * (radius * 0.7); | |
| const labelY = centerY + Math.sin(labelAngle) * (radius * 0.7); | |
| ctx.fillStyle = 'white'; | |
| ctx.font = 'bold 12px Arial'; | |
| ctx.textAlign = 'center'; | |
| ctx.fillText(`${((value / total) * 100).toFixed(1)}%`, labelX, labelY); | |
| currentAngle += sliceAngle; | |
| }); | |
| // Legende zeichnen | |
| let legendY = 20; | |
| labels.forEach((label, i) => { | |
| ctx.fillStyle = colors[i]; | |
| ctx.fillRect(10, legendY, 15, 15); | |
| ctx.fillStyle = '#333'; | |
| ctx.font = '12px Arial'; | |
| ctx.textAlign = 'left'; | |
| ctx.fillText(`${label}: ${formatCurrency(data[i])}`, 30, legendY + 12); | |
| legendY += 20; | |
| }); | |
| } | |
| function drawMonthlyChart() { | |
| const canvas = document.getElementById('monthlyChart'); | |
| const ctx = canvas.getContext('2d'); | |
| // Letzte 6 Monate berechnen | |
| const months = []; | |
| const incomeData = []; | |
| const expenseData = []; | |
| for (let i = 5; i >= 0; i--) { | |
| const date = new Date(); | |
| date.setMonth(date.getMonth() - i); | |
| const monthKey = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`; | |
| months.push(date.toLocaleDateString('de-DE', { month: 'short', year: 'numeric' })); | |
| const monthIncome = transactions | |
| .filter(t => t.type === 'income' && t.date.startsWith(monthKey)) | |
| .reduce((sum, t) => sum + t.amount, 0); | |
| const monthExpense = transactions | |
| .filter(t => t.type === 'expense' && t.date.startsWith(monthKey)) | |
| .reduce((sum, t) => sum + t.amount, 0); | |
| incomeData.push(monthIncome); | |
| expenseData.push(monthExpense); | |
| } | |
| // Canvas-Größe setzen | |
| canvas.width = canvas.offsetWidth; | |
| canvas.height = 300; | |
| const padding = 40; | |
| const chartWidth = canvas.width - padding * 2; | |
| const chartHeight = canvas.height - padding * 2; | |
| const barWidth = chartWidth / (months.length * 2 + months.length - 1); | |
| const maxValue = Math.max(...incomeData, ...expenseData, 1); | |
| // Achsen zeichnen | |
| ctx.strokeStyle = '#ddd'; | |
| ctx.lineWidth = 1; | |
| ctx.beginPath(); | |
| ctx.moveTo(padding, padding); | |
| ctx.lineTo(padding, canvas.height - padding); | |
| ctx.lineTo(canvas.width - padding, canvas.height - padding); | |
| ctx.stroke(); | |
| // Balken zeichnen | |
| months.forEach((month, i) => { | |
| const x = padding + i * (barWidth * 2 + barWidth); | |
| // Einnahmen-Balken | |
| const incomeHeight = (incomeData[i] / maxValue) * chartHeight; | |
| ctx.fillStyle = '#10b981'; | |
| ctx.fillRect(x, canvas.height - padding - incomeHeight, barWidth, incomeHeight); | |
| // Ausgaben-Balken | |
| const expenseHeight = (expenseData[i] / maxValue) * chartHeight; | |
| ctx.fillStyle = '#ef4444'; | |
| ctx.fillRect(x + barWidth, canvas.height - padding - expenseHeight, barWidth, expenseHeight); | |
| // Monats-Label | |
| ctx.fillStyle = '#666'; | |
| ctx.font = '11px Arial'; | |
| ctx.textAlign = 'center'; | |
| ctx.fillText(month, x + barWidth, canvas.height - padding + 20); | |
| }); | |
| // Legende | |
| ctx.fillStyle = '#10b981'; | |
| ctx.fillRect(canvas.width - 100, 10, 15, 15); | |
| ctx.fillStyle = '#333'; | |
| ctx.font = '12px Arial'; | |
| ctx.textAlign = 'left'; | |
| ctx.fillText('Einnahmen', canvas.width - 80, 22); | |
| ctx.fillStyle = '#ef4444'; | |
| ctx.fillRect(canvas.width - 100, 30, 15, 15); | |
| ctx.fillStyle = '#333'; | |
| ctx.fillText('Ausgaben', canvas.width - 80, 42); | |
| } | |
| function exportToCSV() { | |
| if (transactions.length === 0) { | |
| showToast('Keine Daten zum Exportieren', 'error'); | |
| return; | |
| } | |
| let csv = 'Typ,Betrag,Kategorie,Datum,Beschreibung\n'; | |
| transactions.forEach(t => { | |
| csv += `${t.type === 'income' ? 'Einnahme' : 'Ausgabe'},${t.amount},${t.category},${t.date},"${t.description || ''}"\n`; | |
| }); | |
| const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' }); | |
| const link = document.createElement('a'); | |
| link.href = URL.createObjectURL(blob); | |
| link.download = `haushaltsbuch_${new Date().toISOString().split('T')[0]}.csv`; | |
| link.click(); | |
| showToast('CSV erfolgreich exportiert', 'success'); | |
| } | |
| function importFromCSV(event) { | |
| const file = event.target.files[0]; | |
| if (!file) return; | |
| const reader = new FileReader(); | |
| reader.onload = function(e) { | |
| try { | |
| const lines = e.target.result.split('\n'); | |
| const imported = []; | |
| for (let i = 1; i < lines.length; i++) { | |
| if (lines[i].trim() === '') continue; | |
| const parts = lines[i].split(','); | |
| if (parts.length >= 4) { | |
| const transaction = { | |
| id: Date.now() + i, | |
| type: parts[0].trim() === 'Einnahme' ? 'income' : 'expense', | |
| amount: parseFloat(parts[1]), | |
| category: parts[2].trim(), | |
| date: parts[3].trim(), | |
| description: parts[4] ? parts[4].replace(/"/g, '').trim() : '', | |
| timestamp: new Date().toISOString() | |
| }; | |
| imported.push(transaction); | |
| } | |
| } | |
| if (imported.length > 0) { | |
| transactions = [...transactions, ...imported]; | |
| saveTransactions(); | |
| updateUI(); | |
| showToast(`${imported.length} Buchungen importiert`, 'success'); | |
| } else { | |
| showToast('Keine gültigen Daten gefunden', 'error'); | |
| } | |
| } catch (error) { | |
| showToast('Fehler beim Importieren der Datei', 'error'); | |
| } | |
| }; | |
| reader.readAsText(file); | |
| // File input zurücksetzen | |
| event.target.value = ''; | |
| } | |
| function clearAllTransactions() { | |
| if (confirm('Möchten Sie alle Buchungen wirklich löschen?')) { | |
| transactions = []; | |
| saveTransactions(); | |
| updateUI(); | |
| showToast('Alle Buchungen gelöscht', 'success'); | |
| } | |
| } | |
| function clearAllData() { | |
| if (confirm('Möchten Sie wirklich alle Daten löschen? Diese Aktion kann nicht rückgängig gemacht werden!')) { | |
| localStorage.clear(); | |
| transactions = []; | |
| updateUI(); | |
| showToast('Alle Daten gelöscht', 'success'); | |
| } | |
| } | |
| function formatCurrency(amount) { | |
| return new Intl.NumberFormat('de-DE', { | |
| style: 'currency', | |
| currency: 'EUR' | |
| }).format(amount); | |
| } | |
| function formatDate(dateString) { | |
| return new Date(dateString).toLocaleDateString('de-DE', { | |
| day: '2-digit', | |
| month: '2-digit', | |
| year: 'numeric' | |
| }); | |
| } | |
| function showToast(message, type = 'success') { | |
| const toast = document.getElementById('toast'); | |
| const toastMessage = document.getElementById('toastMessage'); | |
| toast.className = `toast ${type}`; | |
| toastMessage.textContent = message; | |
| toast.classList.add('show'); | |
| setTimeout(() => { | |
| toast.classList.remove('show'); | |
| }, 300 |