diff --git a/app.py b/app.py index e036b27..385045b 100644 --- a/app.py +++ b/app.py @@ -125,6 +125,7 @@ class Expense(db.Model): amount = db.Column(db.Float, nullable=False) added_at = db.Column(db.DateTime, default=datetime.utcnow) receipt_filename = db.Column(db.String(255), nullable=True) + list = db.relationship("ShoppingList", backref="expenses", lazy=True) with app.app_context(): @@ -614,7 +615,7 @@ def logout(): @login_required def create_list(): title = request.form.get("title") - is_temporary = "temporary" in request.form + is_temporary = request.form.get("temporary") == "1" token = generate_share_token(8) expires_at = datetime.utcnow() + timedelta(days=7) if is_temporary else None new_list = ShoppingList( @@ -654,6 +655,76 @@ def view_list(list_id): ) +@app.route("/user_expenses") +@login_required +def user_expenses(): + from sqlalchemy.orm import joinedload + + expenses = ( + Expense.query + .join(ShoppingList, Expense.list_id == ShoppingList.id) + .options(joinedload(Expense.list)) + .filter(ShoppingList.owner_id == current_user.id) + .order_by(Expense.added_at.desc()) + .all() + ) + + rows = [ + { + "title": e.list.title if e.list else "Nieznana", + "amount": e.amount, + "added_at": e.added_at + } + for e in expenses + ] + + return render_template("user_expenses.html", expense_table=rows) + + + +@app.route("/user/expenses_data") +@login_required +def user_expenses_data(): + range_type = request.args.get("range", "monthly") + start_date = request.args.get("start_date") + end_date = request.args.get("end_date") + + query = ( + Expense.query + .join(ShoppingList, Expense.list_id == ShoppingList.id) + .filter(ShoppingList.owner_id == current_user.id) + ) + + if start_date and end_date: + try: + start = datetime.strptime(start_date, "%Y-%m-%d") + end = datetime.strptime(end_date, "%Y-%m-%d") + timedelta(days=1) + query = query.filter(Expense.timestamp >= start, Expense.timestamp < end) + except ValueError: + return jsonify({"error": "Błędne daty"}), 400 + + expenses = query.all() + + grouped = defaultdict(float) + for e in expenses: + ts = e.added_at or datetime.utcnow() + if range_type == "monthly": + key = ts.strftime("%Y-%m") + elif range_type == "quarterly": + key = f"{ts.year}-Q{((ts.month - 1) // 3) + 1}" + elif range_type == "halfyearly": + key = f"{ts.year}-H{1 if ts.month <= 6 else 2}" + elif range_type == "yearly": + key = str(ts.year) + else: + key = ts.strftime("%Y-%m-%d") + grouped[key] += e.amount + + labels = sorted(grouped) + data = [round(grouped[label], 2) for label in labels] + return jsonify({"labels": labels, "expenses": data}) + + @app.route("/share/") @app.route("/guest-list/") def shared_list(token=None, list_id=None): @@ -1095,7 +1166,6 @@ def edit_list(list_id): users = User.query.all() items = Item.query.filter_by(list_id=list_id).order_by(Item.id.desc()).all() - # Pobranie listy plików paragonów receipt_pattern = f"list_{list_id}_" all_files = os.listdir(app.config["UPLOAD_FOLDER"]) receipts = [f for f in all_files if f.startswith(receipt_pattern)] @@ -1108,6 +1178,8 @@ def edit_list(list_id): new_amount_str = request.form.get("amount") is_archived = "archived" in request.form is_public = "public" in request.form + is_temporary = "temporary" in request.form + expires_at_raw = request.form.get("expires_at") new_owner_id = request.form.get("owner_id") if new_title: @@ -1115,6 +1187,15 @@ def edit_list(list_id): l.is_archived = is_archived l.is_public = is_public + l.is_temporary = is_temporary + + if expires_at_raw: + try: + l.expires_at = datetime.strptime(expires_at_raw, "%Y-%m-%dT%H:%M") + except ValueError: + l.expires_at = None + else: + l.expires_at = None if new_owner_id: try: diff --git a/static/js/user_expenses.js b/static/js/user_expenses.js new file mode 100644 index 0000000..a029e70 --- /dev/null +++ b/static/js/user_expenses.js @@ -0,0 +1,90 @@ +document.addEventListener("DOMContentLoaded", function () { + let expensesChart = null; + const rangeLabel = document.getElementById("chartRangeLabel"); + + function loadExpenses(range = "monthly", startDate = null, endDate = null) { + let url = '/user/expenses_data?range=' + range; + if (startDate && endDate) { + url += `&start_date=${startDate}&end_date=${endDate}`; + } + + fetch(url, { cache: "no-store" }) + .then(response => response.json()) + .then(data => { + const ctx = document.getElementById('expensesChart').getContext('2d'); + + if (expensesChart) { + expensesChart.destroy(); + } + + expensesChart = new Chart(ctx, { + type: 'bar', + data: { + labels: data.labels, + datasets: [{ + label: 'Suma wydatków [PLN]', + data: data.expenses, + backgroundColor: '#0d6efd' + }] + }, + options: { + scales: { + y: { + beginAtZero: true + } + } + } + }); + + if (startDate && endDate) { + rangeLabel.textContent = `Widok: własny zakres (${startDate} → ${endDate})`; + } else { + let labelText = ""; + if (range === "monthly") labelText = "Widok: miesięczne"; + else if (range === "quarterly") labelText = "Widok: kwartalne"; + else if (range === "halfyearly") labelText = "Widok: półroczne"; + else if (range === "yearly") labelText = "Widok: roczne"; + rangeLabel.textContent = labelText; + } + + }) + .catch(error => { + console.error("Błąd pobierania danych:", error); + }); + } + + // Inicjalizacja zakresu dat + const startDateInput = document.getElementById("startDate"); + const endDateInput = document.getElementById("endDate"); + const today = new Date(); + const lastWeek = new Date(today); + lastWeek.setDate(today.getDate() - 7); + const formatDate = (d) => d.toISOString().split('T')[0]; + startDateInput.value = formatDate(lastWeek); + endDateInput.value = formatDate(today); + + // Załaduj początkowy widok + loadExpenses(); + + // Przycisk własnego zakresu + document.getElementById('customRangeBtn').addEventListener('click', function () { + const startDate = startDateInput.value; + const endDate = endDateInput.value; + if (startDate && endDate) { + document.querySelectorAll('.range-btn').forEach(b => b.classList.remove('active')); + loadExpenses('custom', startDate, endDate); + } else { + alert("Proszę wybrać obie daty!"); + } + }); + + // Zakresy predefiniowane + document.querySelectorAll('.range-btn').forEach(btn => { + btn.addEventListener('click', function () { + document.querySelectorAll('.range-btn').forEach(b => b.classList.remove('active')); + this.classList.add('active'); + const range = this.getAttribute('data-range'); + loadExpenses(range); + }); + }); +}); diff --git a/templates/admin/edit_list.html b/templates/admin/edit_list.html index ac59b53..8434638 100644 --- a/templates/admin/edit_list.html +++ b/templates/admin/edit_list.html @@ -46,6 +46,19 @@ +
+ + +
+ +
+ + +
+ +
- -
diff --git a/templates/base.html b/templates/base.html index 0cb3554..146dbd8 100644 --- a/templates/base.html +++ b/templates/base.html @@ -35,21 +35,22 @@ {% endif %} {% endif %} - {% if not is_blocked %} -
- {% if request.endpoint and request.endpoint != 'system_auth' %} - {% if current_user.is_authenticated and current_user.is_admin %} - ⚙️ Panel admina - {% endif %} + {% if not is_blocked and request.endpoint and request.endpoint != 'system_auth' %} +
{% if current_user.is_authenticated %} + {% if current_user.is_admin %} + ⚙️ Panel admina + {% endif %} + 📊 Statystyki 🚪 Wyloguj {% else %} 🔑 Zaloguj {% endif %} - {% endif %}
{% endif %} + +
diff --git a/templates/user_expenses.html b/templates/user_expenses.html new file mode 100644 index 0000000..d3ebb1a --- /dev/null +++ b/templates/user_expenses.html @@ -0,0 +1,84 @@ +{% extends 'base.html' %} +{% block title %}📊 Twoje wydatki{% endblock %} + +{% block content %} +
+

📊 Statystyki wydatków

+ ← Powrót +
+ + + +
+ +
+
+
+ {% if expense_table %} +
+ {% for row in expense_table %} +
+
+
+
{{ row.title }}
+

💸 {{ '%.2f'|format(row.amount) }} PLN

+

📅 {{ row.added_at.strftime('%Y-%m-%d') }}

+
+
+
+ {% endfor %} +
+ {% else %} +
Brak wydatków do wyświetlenia.
+ {% endif %} +
+
+
+ + +
+
+
+

Widok: miesięczne

+ +
+
+ +
+ + + + +
+ +
+
+ +
+
+ +
+
+ +
+
+
+
+{% endblock %} + +{% block scripts %} + + +{% endblock %} \ No newline at end of file