Initial commit

This commit is contained in:
2025-05-31 14:50:40 +00:00
commit f61df12e5d
14 changed files with 1128 additions and 0 deletions

29
.gitignore vendored Normal file
View File

@ -0,0 +1,29 @@
# Python virtual environment
venv/
env/
ENV/
.env/
.venv/
# Flask instance folder
instance/
# Python cache files
__pycache__/
*.py[cod]
*$py.class
# Distribution / packaging
dist/
build/
*.egg-info/
# Local development settings
.env
.flaskenv
# IDE specific files
.idea/
.vscode/
*.swp
*.swo

30
Dockerfile Normal file
View File

@ -0,0 +1,30 @@
ARG BUILD_FROM
FROM $BUILD_FROM
# Set shell
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
# Install required packages
RUN apk add --no-cache \
python3 \
py3-pip \
py3-wheel \
gcc \
python3-dev \
musl-dev
# Set working directory
WORKDIR /app
# Copy files
COPY requirements.txt .
COPY . .
# Install Python requirements
RUN pip3 install --no-cache-dir -r requirements.txt
# Expose the port used for the web interface
EXPOSE 8099
# Start the app
CMD ["python3", "main.py"]

95
README.md Normal file
View File

@ -0,0 +1,95 @@
# Expense Tracker Add-on für Home Assistant
Dieses Home Assistant Add-on bietet eine komfortable Möglichkeit, persönliche oder Haushaltsausgaben direkt innerhalb von Home Assistant zu verfolgen und zu analysieren.
## Features
- **Erfassung von Ausgaben**: Erfasse manuell oder automatisiert via API deine täglichen, wöchentlichen oder monatlichen Ausgaben.
- **Kategorisierung**: Ordne Ausgaben in selbst definierbare Kategorien wie Lebensmittel, Transport, Freizeit, etc., um einen besseren Überblick über dein Budget zu erhalten.
- **Integration in Dashboards**: Binde Ausgabendiagramme und Statistiken direkt in deine Home Assistant Dashboards ein.
- **Datensicherheit**: Alle Daten bleiben lokal in deinem Home Assistant-System und werden nicht an Dritte weitergegeben.
## Installation
1. Navigiere in Home Assistant zu **Einstellungen****Add-ons****Add-on Store**
2. Klicke auf die drei Punkte in der oberen rechten Ecke und wähle **Repositories**
3. Füge die URL dieses Repositories hinzu
4. Suche nach "Expense Tracker" in der Add-on Liste und installiere es
5. Starte das Add-on und aktiviere "Show in sidebar"
## Verwendung
### Ausgaben erfassen
1. Öffne das Add-on über die Seitenleiste
2. Navigiere zum Tab "Ausgaben"
3. Fülle das Formular aus mit:
- Betrag
- Beschreibung
- Kategorie
- Datum
4. Klicke auf "Ausgabe speichern"
### Kategorien verwalten
1. Navigiere zum Tab "Kategorien"
2. Füge neue Kategorien hinzu oder sieh dir bestehende an
### Berichte anzeigen
1. Navigiere zum Tab "Berichte"
2. Filtere nach Zeitraum, um spezifische Daten anzuzeigen
3. Betrachte die Ausgabenverteilung nach Kategorien und den zeitlichen Verlauf
## API-Integration
Das Add-on bietet eine REST-API zur Integration mit anderen Systemen:
### Ausgaben hinzufügen
```
POST /api/expenses
Content-Type: application/json
{
"amount": 12.99,
"description": "Mittagessen",
"category_id": 1,
"date": "2025-05-30T12:00:00"
}
```
### Ausgaben abrufen
```
GET /api/expenses
```
### Kategorien abrufen
```
GET /api/categories
```
## Dashboard-Integration
Um Expense Tracker-Widgets in dein Home Assistant Dashboard einzubinden, kannst du die iframe-Karte verwenden:
```yaml
type: iframe
url: /api/hassio_ingress/self_slug
aspect_ratio: 75%
title: Ausgaben
```
## Daten-Backup
Die Datenbank wird im `/data`-Verzeichnis des Add-ons gespeichert und wird automatisch in den regulären Home Assistant-Backups gesichert.
## Support
Bei Fragen oder Problemen öffne bitte ein Issue im GitHub-Repository.
## Lizenz
Dieses Projekt ist unter der MIT-Lizenz veröffentlicht.

23
config.yaml Normal file
View File

@ -0,0 +1,23 @@
name: "Expense Tracker"
description: "Track and analyze personal or household expenses within Home Assistant"
version: "1.0.0"
slug: "expense_tracker"
init: false
arch:
- aarch64
- amd64
- armhf
- armv7
- i386
startup: application
ingress: true
ingress_port: 8099
panel_icon: mdi:cash-multiple
panel_title: "Expense Tracker"
panel_admin: true
options:
database_path: "/data/expense_tracker.db"
log_level: "info"
schema:
database_path: str
log_level: list(trace|debug|info|warning|error|fatal)

276
main.py Normal file
View File

@ -0,0 +1,276 @@
#!/usr/bin/env python3
import os
import json
import logging
import sqlite3
from datetime import datetime
from flask import Flask, request, jsonify, render_template, redirect, url_for
from flask_sqlalchemy import SQLAlchemy
import plotly.express as px
import pandas as pd
# Setup logging
logging.basicConfig(
level=os.environ.get("LOG_LEVEL", "INFO"),
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
)
logger = logging.getLogger("expense_tracker")
# Load configuration
try:
with open("/data/options.json", "r") as options_file:
config = json.load(options_file)
except Exception as e:
logger.warning(f"Failed to load options.json, using defaults: {e}")
config = {
"database_path": "expense_tracker.db", # Local file in current directory
"log_level": "info",
}
# Set log level from config
log_level = getattr(logging, config["log_level"].upper(), logging.INFO)
logger.setLevel(log_level)
# Initialize Flask app
app = Flask(__name__)
app.config["SECRET_KEY"] = os.urandom(24)
app.config["SQLALCHEMY_DATABASE_URI"] = f"sqlite:///{config['database_path']}"
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
# Initialize database
db = SQLAlchemy(app)
# Define database models
class Category(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(50), nullable=False, unique=True)
description = db.Column(db.String(200))
expenses = db.relationship("Expense", backref="category", lazy=True)
class Expense(db.Model):
id = db.Column(db.Integer, primary_key=True)
amount = db.Column(db.Float, nullable=False)
description = db.Column(db.String(200))
date = db.Column(db.DateTime, nullable=False, default=datetime.now)
category_id = db.Column(db.Integer, db.ForeignKey("category.id"), nullable=False)
# Create database tables
with app.app_context():
db.create_all()
# Add default categories if none exist
if Category.query.count() == 0:
default_categories = [
Category(name="Lebensmittel", description="Ausgaben für Lebensmittel und Getränke"),
Category(name="Transport", description="Ausgaben für öffentliche Verkehrsmittel, Taxi, etc."),
Category(name="Freizeit", description="Ausgaben für Unterhaltung, Hobbys, etc."),
Category(name="Wohnen", description="Miete, Nebenkosten, Reparaturen"),
Category(name="Gesundheit", description="Medikamente, Arztbesuche"),
Category(name="Sonstiges", description="Sonstige Ausgaben")
]
for category in default_categories:
db.session.add(category)
db.session.commit()
logger.info("Added default categories")
# Routes
@app.route("/")
def index():
categories = Category.query.all()
expenses = Expense.query.order_by(Expense.date.desc()).limit(10).all()
# Calculate total expenses
total_expenses = db.session.query(db.func.sum(Expense.amount)).scalar() or 0
# Calculate expenses by category
category_expenses = db.session.query(
Category.name, db.func.sum(Expense.amount)
).join(Expense).group_by(Category.name).all()
# Prepare data for charts
category_names = [item[0] for item in category_expenses]
category_amounts = [float(item[1]) for item in category_expenses]
return render_template(
"index.html",
categories=categories,
expenses=expenses,
total_expenses=total_expenses,
category_names=json.dumps(category_names),
category_amounts=json.dumps(category_amounts),
now=datetime.now()
)
@app.route("/expenses", methods=["GET", "POST"])
def expenses():
if request.method == "POST":
# Add new expense
amount = float(request.form["amount"])
description = request.form["description"]
category_id = int(request.form["category_id"])
date_str = request.form["date"]
# Parse date from form
date = datetime.strptime(date_str, "%Y-%m-%d")
# Create new expense
expense = Expense(
amount=amount,
description=description,
category_id=category_id,
date=date
)
db.session.add(expense)
db.session.commit()
return redirect(url_for("expenses"))
# GET request - show expenses and form
categories = Category.query.all()
expenses = Expense.query.order_by(Expense.date.desc()).all()
return render_template(
"expenses.html",
categories=categories,
expenses=expenses,
today=datetime.now().strftime("%Y-%m-%d")
)
@app.route("/categories", methods=["GET", "POST"])
def categories():
if request.method == "POST":
# Add new category
name = request.form["name"]
description = request.form["description"]
# Check if category already exists
existing = Category.query.filter_by(name=name).first()
if existing:
return render_template(
"categories.html",
categories=Category.query.all(),
error="Kategorie existiert bereits"
)
# Create new category
category = Category(name=name, description=description)
db.session.add(category)
db.session.commit()
return redirect(url_for("categories"))
# GET request - show categories and form
categories = Category.query.all()
return render_template("categories.html", categories=categories)
@app.route("/api/expenses", methods=["GET", "POST"])
def api_expenses():
if request.method == "POST":
# Add new expense via API
data = request.json
if not data or "amount" not in data or "category_id" not in data:
return jsonify({"error": "Invalid data"}), 400
# Create new expense
expense = Expense(
amount=float(data["amount"]),
description=data.get("description", ""),
category_id=int(data["category_id"]),
date=datetime.now() if "date" not in data else datetime.fromisoformat(data["date"])
)
db.session.add(expense)
db.session.commit()
return jsonify({"id": expense.id, "status": "success"}), 201
# GET request - return expenses as JSON
expenses = Expense.query.order_by(Expense.date.desc()).all()
result = []
for expense in expenses:
result.append({
"id": expense.id,
"amount": expense.amount,
"description": expense.description,
"date": expense.date.isoformat(),
"category_id": expense.category_id,
"category_name": expense.category.name
})
return jsonify(result)
@app.route("/api/categories", methods=["GET"])
def api_categories():
categories = Category.query.all()
result = []
for category in categories:
result.append({
"id": category.id,
"name": category.name,
"description": category.description
})
return jsonify(result)
@app.route("/reports")
def reports():
# Get date range filter
start_date = request.args.get("start_date")
end_date = request.args.get("end_date")
query = db.session.query(
Expense.date,
Category.name.label("category"),
Expense.amount
).join(Category)
# Apply date filters if provided
if start_date:
query = query.filter(Expense.date >= datetime.strptime(start_date, "%Y-%m-%d"))
if end_date:
query = query.filter(Expense.date <= datetime.strptime(end_date, "%Y-%m-%d"))
# Execute query and convert to DataFrame
expenses_data = query.all()
df = pd.DataFrame(expenses_data, columns=["date", "category", "amount"])
# Generate charts
if not df.empty:
# Expenses by category pie chart
fig_category = px.pie(
df,
values="amount",
names="category",
title="Ausgaben nach Kategorie"
)
category_chart = fig_category.to_html(full_html=False)
# Expenses over time line chart
df["date"] = pd.to_datetime(df["date"])
df_by_date = df.groupby([df["date"].dt.date]).sum().reset_index()
fig_timeline = px.line(
df_by_date,
x="date",
y="amount",
title="Ausgaben im Zeitverlauf"
)
timeline_chart = fig_timeline.to_html(full_html=False)
else:
category_chart = "<p>Keine Daten verfügbar</p>"
timeline_chart = "<p>Keine Daten verfügbar</p>"
return render_template(
"reports.html",
category_chart=category_chart,
timeline_chart=timeline_chart,
start_date=start_date or "",
end_date=end_date or ""
)
if __name__ == "__main__":
# Start the Flask application
app.run(host="0.0.0.0", port=8099)

7
requirements.txt Normal file
View File

@ -0,0 +1,7 @@
flask==2.3.3
flask-sqlalchemy==3.0.5
paho-mqtt==2.2.1
plotly==5.16.1
pandas==2.0.3
werkzeug==2.3.7
gunicorn==21.2.0

12
run.sh Executable file
View File

@ -0,0 +1,12 @@
#!/usr/bin/with-contenv bashio
# Set up environment variables
export LOG_LEVEL=$(bashio::config 'log_level')
export DATABASE_PATH=$(bashio::config 'database_path')
# Ensure data directory exists
mkdir -p "$(dirname "$DATABASE_PATH")"
# Start the Flask application with gunicorn
cd /app
exec gunicorn --bind 0.0.0.0:8099 --workers 2 --threads 4 main:app

303
static/css/styles.css Normal file
View File

@ -0,0 +1,303 @@
:root {
--primary-color: #03a9f4;
--secondary-color: #4caf50;
--background-color: #f5f5f5;
--card-background: #ffffff;
--text-color: #212121;
--text-secondary: #757575;
--border-color: #e0e0e0;
--sidebar-width: 250px;
--header-height: 60px;
--shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Roboto', 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: var(--background-color);
color: var(--text-color);
line-height: 1.6;
}
/* Sidebar */
.sidebar {
position: fixed;
width: var(--sidebar-width);
height: 100vh;
background-color: var(--card-background);
box-shadow: var(--shadow);
z-index: 100;
overflow-y: auto;
}
.sidebar-header {
padding: 20px;
display: flex;
align-items: center;
border-bottom: 1px solid var(--border-color);
}
.sidebar-header i {
font-size: 24px;
color: var(--primary-color);
margin-right: 10px;
}
.sidebar-header h2 {
font-size: 18px;
font-weight: 500;
}
.sidebar nav ul {
list-style: none;
}
.sidebar nav ul li a {
display: flex;
align-items: center;
padding: 15px 20px;
color: var(--text-color);
text-decoration: none;
transition: background-color 0.3s;
}
.sidebar nav ul li a:hover {
background-color: rgba(0, 0, 0, 0.05);
}
.sidebar nav ul li a i {
margin-right: 10px;
color: var(--primary-color);
}
/* Main Content */
.main-content {
margin-left: var(--sidebar-width);
padding: 20px;
}
.content-wrapper {
max-width: 1200px;
margin: 0 auto;
}
/* Dashboard */
.dashboard h1, .expenses-page h1, .categories-page h1, .reports-page h1 {
margin-bottom: 20px;
font-weight: 500;
}
.dashboard-summary {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 20px;
margin-bottom: 30px;
}
.summary-card {
background-color: var(--card-background);
border-radius: 8px;
padding: 20px;
box-shadow: var(--shadow);
display: flex;
align-items: center;
}
.card-icon {
width: 50px;
height: 50px;
border-radius: 50%;
background-color: rgba(3, 169, 244, 0.1);
display: flex;
align-items: center;
justify-content: center;
margin-right: 15px;
}
.card-icon i {
font-size: 24px;
color: var(--primary-color);
}
.card-content h3 {
font-size: 16px;
font-weight: 500;
margin-bottom: 5px;
color: var(--text-secondary);
}
.card-content p {
font-size: 20px;
font-weight: 500;
}
.card-content p.amount {
color: var(--primary-color);
}
/* Charts */
.dashboard-charts, .charts-container {
margin-bottom: 30px;
}
.chart-container, .chart-box {
background-color: var(--card-background);
border-radius: 8px;
padding: 20px;
box-shadow: var(--shadow);
margin-bottom: 20px;
}
.chart-container h2, .chart-box h2 {
font-size: 18px;
font-weight: 500;
margin-bottom: 15px;
}
/* Tables */
table {
width: 100%;
border-collapse: collapse;
margin-top: 10px;
}
table th, table td {
padding: 12px 15px;
text-align: left;
border-bottom: 1px solid var(--border-color);
}
table th {
font-weight: 500;
color: var(--text-secondary);
}
table td.amount {
font-weight: 500;
color: var(--primary-color);
}
table td.no-data {
text-align: center;
color: var(--text-secondary);
padding: 30px 0;
}
/* Forms */
.form-container {
background-color: var(--card-background);
border-radius: 8px;
padding: 20px;
box-shadow: var(--shadow);
margin-bottom: 30px;
}
.form-container h2 {
font-size: 18px;
font-weight: 500;
margin-bottom: 20px;
}
.form-group {
margin-bottom: 15px;
}
.form-group.inline {
display: inline-block;
margin-right: 15px;
}
.form-group label {
display: block;
margin-bottom: 5px;
font-weight: 500;
}
.form-group input, .form-group select {
width: 100%;
padding: 10px;
border: 1px solid var(--border-color);
border-radius: 4px;
font-size: 14px;
}
.form-actions {
margin-top: 20px;
}
.button {
display: inline-block;
padding: 10px 15px;
background-color: var(--primary-color);
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
text-decoration: none;
font-size: 14px;
font-weight: 500;
transition: background-color 0.3s;
}
.button:hover {
background-color: #0288d1;
}
.button.secondary {
background-color: #9e9e9e;
}
.button.secondary:hover {
background-color: #757575;
}
.action-button {
margin-top: 15px;
text-align: right;
}
/* Error messages */
.error-message {
background-color: #ffebee;
color: #c62828;
padding: 10px;
border-radius: 4px;
margin-bottom: 15px;
}
/* Filter container */
.filter-container {
background-color: var(--card-background);
border-radius: 8px;
padding: 20px;
box-shadow: var(--shadow);
margin-bottom: 30px;
}
.filter-container h2 {
font-size: 18px;
font-weight: 500;
margin-bottom: 15px;
}
/* Responsive design */
@media (max-width: 768px) {
.sidebar {
width: 100%;
height: auto;
position: relative;
}
.main-content {
margin-left: 0;
}
.dashboard-summary {
grid-template-columns: 1fr;
}
}

43
static/js/scripts.js Normal file
View File

@ -0,0 +1,43 @@
document.addEventListener('DOMContentLoaded', function() {
// Add current date to the dashboard
const now = new Date();
const months = [
'Januar', 'Februar', 'März', 'April', 'Mai', 'Juni',
'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember'
];
// Format date for display
const formattedDate = `${months[now.getMonth()]} ${now.getFullYear()}`;
// Add to any elements with date-display class
const dateElements = document.querySelectorAll('.date-display');
dateElements.forEach(element => {
element.textContent = formattedDate;
});
// Handle form validation
const forms = document.querySelectorAll('form');
forms.forEach(form => {
form.addEventListener('submit', function(event) {
const amountInput = form.querySelector('input[name="amount"]');
if (amountInput && parseFloat(amountInput.value) <= 0) {
event.preventDefault();
alert('Bitte geben Sie einen gültigen Betrag ein (größer als 0).');
}
});
});
// Add responsive menu toggle for mobile
const menuToggle = document.createElement('button');
menuToggle.classList.add('menu-toggle');
menuToggle.innerHTML = '<i class="fas fa-bars"></i>';
const sidebar = document.querySelector('.sidebar');
if (sidebar) {
sidebar.parentNode.insertBefore(menuToggle, sidebar);
menuToggle.addEventListener('click', function() {
sidebar.classList.toggle('active');
});
}
});

35
templates/base.html Normal file
View File

@ -0,0 +1,35 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Expense Tracker - Home Assistant</title>
<link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}">
<script src="https://cdn.plot.ly/plotly-latest.min.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
</head>
<body>
<div class="sidebar">
<div class="sidebar-header">
<i class="fas fa-cash-register"></i>
<h2>Expense Tracker</h2>
</div>
<nav>
<ul>
<li><a href="{{ url_for('index') }}"><i class="fas fa-home"></i> Dashboard</a></li>
<li><a href="{{ url_for('expenses') }}"><i class="fas fa-receipt"></i> Ausgaben</a></li>
<li><a href="{{ url_for('categories') }}"><i class="fas fa-tags"></i> Kategorien</a></li>
<!--<li><a href="{{ url_for('reports') }}"><i class="fas fa-chart-pie"></i> Berichte</a></li>-->
</ul>
</nav>
</div>
<div class="main-content">
<div class="content-wrapper">
{% block content %}{% endblock %}
</div>
</div>
<script src="{{ url_for('static', filename='js/scripts.js') }}"></script>
</body>
</html>

53
templates/categories.html Normal file
View File

@ -0,0 +1,53 @@
{% extends "base.html" %}
{% block content %}
<div class="categories-page">
<h1>Kategorien verwalten</h1>
<div class="form-container">
<h2>Neue Kategorie hinzufügen</h2>
{% if error %}
<div class="error-message">{{ error }}</div>
{% endif %}
<form action="{{ url_for('categories') }}" method="post">
<div class="form-group">
<label for="name">Name:</label>
<input type="text" id="name" name="name" required>
</div>
<div class="form-group">
<label for="description">Beschreibung:</label>
<input type="text" id="description" name="description">
</div>
<div class="form-actions">
<button type="submit" class="button primary">Kategorie speichern</button>
</div>
</form>
</div>
<div class="categories-list">
<h2>Alle Kategorien</h2>
<table>
<thead>
<tr>
<th>Name</th>
<th>Beschreibung</th>
</tr>
</thead>
<tbody>
{% for category in categories %}
<tr>
<td>{{ category.name }}</td>
<td>{{ category.description }}</td>
</tr>
{% else %}
<tr>
<td colspan="2" class="no-data">Keine Kategorien vorhanden</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endblock %}

69
templates/expenses.html Normal file
View File

@ -0,0 +1,69 @@
{% extends "base.html" %}
{% block content %}
<div class="expenses-page">
<h1>Ausgaben verwalten</h1>
<div class="form-container">
<h2>Neue Ausgabe hinzufügen</h2>
<form action="{{ url_for('expenses') }}" method="post">
<div class="form-group">
<label for="amount">Betrag (€):</label>
<input type="number" id="amount" name="amount" step="0.01" min="0.01" required>
</div>
<div class="form-group">
<label for="description">Beschreibung:</label>
<input type="text" id="description" name="description" required>
</div>
<div class="form-group">
<label for="category_id">Kategorie:</label>
<select id="category_id" name="category_id" required>
<option value="">-- Kategorie wählen --</option>
{% for category in categories %}
<option value="{{ category.id }}">{{ category.name }}</option>
{% endfor %}
</select>
</div>
<div class="form-group">
<label for="date">Datum:</label>
<input type="date" id="date" name="date" value="{{ today }}" required>
</div>
<div class="form-actions">
<button type="submit" class="button primary">Ausgabe speichern</button>
</div>
</form>
</div>
<div class="expenses-list">
<h2>Alle Ausgaben</h2>
<table>
<thead>
<tr>
<th>Datum</th>
<th>Beschreibung</th>
<th>Kategorie</th>
<th>Betrag</th>
</tr>
</thead>
<tbody>
{% for expense in expenses %}
<tr>
<td>{{ expense.date.strftime('%d.%m.%Y') }}</td>
<td>{{ expense.description }}</td>
<td>{{ expense.category.name }}</td>
<td class="amount">{{ "%.2f"|format(expense.amount) }} €</td>
</tr>
{% else %}
<tr>
<td colspan="4" class="no-data">Keine Ausgaben vorhanden</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endblock %}

110
templates/index.html Normal file
View File

@ -0,0 +1,110 @@
{% extends "base.html" %}
{% block content %}
<div class="dashboard">
<h1>Dashboard</h1>
<div class="dashboard-summary">
<div class="summary-card">
<div class="card-icon">
<i class="fas fa-euro-sign"></i>
</div>
<div class="card-content">
<h3>Gesamtausgaben</h3>
<p class="amount">{{ "%.2f"|format(total_expenses) }} €</p>
</div>
</div>
<div class="summary-card">
<div class="card-icon">
<i class="fas fa-calendar-alt"></i>
</div>
<div class="card-content">
<h3>Aktuelle Periode</h3>
<p>{{ now.strftime('%B %Y') }}</p>
</div>
</div>
<div class="summary-card">
<div class="card-icon">
<i class="fas fa-tags"></i>
</div>
<div class="card-content">
<h3>Kategorien</h3>
<p>{{ categories|length }}</p>
</div>
</div>
</div>
<div class="dashboard-charts">
<div class="chart-container">
<h2>Ausgaben nach Kategorie</h2>
<div id="category-chart"></div>
</div>
</div>
<div class="recent-expenses">
<h2>Neueste Ausgaben</h2>
<table>
<thead>
<tr>
<th>Datum</th>
<th>Beschreibung</th>
<th>Kategorie</th>
<th>Betrag</th>
</tr>
</thead>
<tbody>
{% for expense in expenses %}
<tr>
<td>{{ expense.date.strftime('%d.%m.%Y') }}</td>
<td>{{ expense.description }}</td>
<td>{{ expense.category.name }}</td>
<td class="amount">{{ "%.2f"|format(expense.amount) }} €</td>
</tr>
{% else %}
<tr>
<td colspan="4" class="no-data">Keine Ausgaben vorhanden</td>
</tr>
{% endfor %}
</tbody>
</table>
<div class="action-button">
<a href="{{ url_for('expenses') }}" class="button">Alle Ausgaben anzeigen</a>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Category pie chart
const categoryNames = {{ category_names|safe }};
const categoryAmounts = {{ category_amounts|safe }};
if (categoryNames.length > 0) {
const data = [{
values: categoryAmounts,
labels: categoryNames,
type: 'pie',
marker: {
colors: [
'#4e79a7', '#f28e2c', '#e15759', '#76b7b2',
'#59a14f', '#edc949', '#af7aa1', '#ff9da7',
'#9c755f', '#bab0ab'
]
}
}];
const layout = {
height: 400,
margin: { t: 0, b: 0, l: 0, r: 0 },
showlegend: true
};
Plotly.newPlot('category-chart', data, layout);
} else {
document.getElementById('category-chart').innerHTML = '<p class="no-data">Keine Daten verfügbar</p>';
}
});
</script>
{% endblock %}

43
templates/reports.html Normal file
View File

@ -0,0 +1,43 @@
{% extends "base.html" %}
{% block content %}
<div class="reports-page">
<h1>Ausgabenberichte</h1>
<div class="filter-container">
<h2>Zeitraum filtern</h2>
<form action="{{ url_for('reports') }}" method="get">
<div class="form-group inline">
<label for="start_date">Von:</label>
<input type="date" id="start_date" name="start_date" value="{{ start_date }}">
</div>
<div class="form-group inline">
<label for="end_date">Bis:</label>
<input type="date" id="end_date" name="end_date" value="{{ end_date }}">
</div>
<div class="form-actions">
<button type="submit" class="button primary">Filter anwenden</button>
<a href="{{ url_for('reports') }}" class="button secondary">Zurücksetzen</a>
</div>
</form>
</div>
<div class="charts-container">
<div class="chart-box">
<h2>Ausgaben nach Kategorie</h2>
<div class="chart">
{{ category_chart|safe }}
</div>
</div>
<div class="chart-box">
<h2>Ausgaben im Zeitverlauf</h2>
<div class="chart">
{{ timeline_chart|safe }}
</div>
</div>
</div>
</div>
{% endblock %}