diff --git a/alters.txt b/alters.txt index d619871..d0c1059 100644 --- a/alters.txt +++ b/alters.txt @@ -10,3 +10,14 @@ ADD COLUMN note TEXT; # NOWE FUNKCJE ADMINA ALTER TABLE shopping_list ADD COLUMN is_archived BOOLEAN DEFAULT FALSE; + + +# FUNKCJA WYDATKOW +CREATE TABLE expense ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + list_id INTEGER, + amount FLOAT NOT NULL, + added_at DATETIME DEFAULT CURRENT_TIMESTAMP, + receipt_filename VARCHAR(255), + FOREIGN KEY(list_id) REFERENCES shopping_list(id) +); \ No newline at end of file diff --git a/app.py b/app.py index 9ab7c03..20df8ec 100644 --- a/app.py +++ b/app.py @@ -12,7 +12,7 @@ from config import Config from PIL import Image from werkzeug.utils import secure_filename from werkzeug.middleware.proxy_fix import ProxyFix -from sqlalchemy import func +from sqlalchemy import func, extract app = Flask(__name__) app.config.from_object(Config) @@ -64,6 +64,13 @@ class SuggestedProduct(db.Model): id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(150), unique=True, nullable=False) +class Expense(db.Model): + id = db.Column(db.Integer, primary_key=True) + list_id = db.Column(db.Integer, db.ForeignKey('shopping_list.id')) + 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) + @static_bp.route('/static/js/live.js') def serve_live_js(): response = send_from_directory('static/js', 'live.js') @@ -161,11 +168,20 @@ def system_auth(): @app.route('/') def index_guest(): - lists = ShoppingList.query.all() + now = datetime.utcnow() + lists = ShoppingList.query.filter( + ((ShoppingList.expires_at == None) | (ShoppingList.expires_at > now)), + ShoppingList.is_archived == False + ).order_by(ShoppingList.created_at.desc()).all() + for l in lists: items = Item.query.filter_by(list_id=l.id).all() l.total_count = len(items) l.purchased_count = len([i for i in items if i.purchased]) + + expenses = Expense.query.filter_by(list_id=l.id).all() + l.total_expense = sum(e.amount for e in expenses) + return render_template('index.html', lists=lists) @app.errorhandler(404) @@ -217,11 +233,11 @@ def create_list(): def view_list(list_id): shopping_list = ShoppingList.query.get_or_404(list_id) items = Item.query.filter_by(list_id=list_id).all() - total_count = len(items) purchased_count = len([i for i in items if i.purchased]) percent = (purchased_count / total_count * 100) if total_count > 0 else 0 - + expenses = Expense.query.filter_by(list_id=list_id).all() + total_expense = sum(e.amount for e in expenses) receipt_pattern = f"list_{list_id}" all_files = os.listdir(app.config['UPLOAD_FOLDER']) receipt_files = [f for f in all_files if receipt_pattern in f] @@ -233,10 +249,11 @@ def view_list(list_id): receipt_files=receipt_files, total_count=total_count, purchased_count=purchased_count, - percent=percent + percent=percent, + expenses=expenses, + total_expense=total_expense ) - @app.route('/share/') def share_list(token): shopping_list = ShoppingList.query.filter_by(share_token=token).first_or_404() @@ -246,21 +263,36 @@ def share_list(token): all_files = os.listdir(app.config['UPLOAD_FOLDER']) receipt_files = [f for f in all_files if receipt_pattern in f] - return render_template('list_guest.html', list=shopping_list, items=items, receipt_files=receipt_files) + expenses = Expense.query.filter_by(list_id=shopping_list.id).all() + total_expense = sum(e.amount for e in expenses) + + return render_template( + 'list_guest.html', + list=shopping_list, + items=items, + receipt_files=receipt_files, + expenses=expenses, + total_expense=total_expense + ) @app.route('/guest-list/') def guest_list(list_id): shopping_list = ShoppingList.query.get_or_404(list_id) items = Item.query.filter_by(list_id=list_id).all() - receipt_pattern = f"list_{list_id}" all_files = os.listdir(app.config['UPLOAD_FOLDER']) - receipt_files = [f for f in all_files if receipt_pattern in f] - - return render_template('list_guest.html', list=shopping_list, items=items, receipt_files=receipt_files) - + expenses = Expense.query.filter_by(list_id=list_id).all() + total_expense = sum(e.amount for e in expenses) + return render_template( + 'list_guest.html', + list=shopping_list, + items=items, + receipt_files=receipt_files, + expenses=expenses, + total_expense=total_expense + ) @app.route('/copy/') @login_required @@ -349,9 +381,8 @@ def admin_panel(): purchased_count = len([i for i in items if i.purchased]) percent = (purchased_count / total_count * 100) if total_count > 0 else 0 comments_count = len([i for i in items if i.note and i.note.strip() != '']) - purchased_items_count = Item.query.filter_by(purchased=True).count() - - + expenses = Expense.query.filter_by(list_id=l.id).all() + total_expense = sum(e.amount for e in expenses) receipt_pattern = f"list_{l.id}" receipt_files = [f for f in all_files if receipt_pattern in f] @@ -361,28 +392,53 @@ def admin_panel(): 'purchased_count': purchased_count, 'percent': round(percent), 'comments_count': comments_count, - 'receipts_count': len(receipt_files) + 'receipts_count': len(receipt_files), + 'total_expense': total_expense }) + # Najczęściej kupowane produkty + top_products = ( + db.session.query(Item.name, func.count(Item.id).label('count')) + .filter(Item.purchased == True) + .group_by(Item.name) + .order_by(func.count(Item.id).desc()) + .limit(5) + .all() + ) - top_products = ( - db.session.query(Item.name, func.count(Item.id).label('count')) - .filter(Item.purchased == True) - .group_by(Item.name) - .order_by(func.count(Item.id).desc()) - .limit(5) - .all() - ) + purchased_items_count = Item.query.filter_by(purchased=True).count() + + # Podsumowanie wydatków + total_expense_sum = db.session.query(func.sum(Expense.amount)).scalar() or 0 + + current_year = datetime.utcnow().year + year_expense_sum = ( + db.session.query(func.sum(Expense.amount)) + .filter(extract('year', Expense.added_at) == current_year) + .scalar() or 0 + ) + + current_month = datetime.utcnow().month + month_expense_sum = ( + db.session.query(func.sum(Expense.amount)) + .filter(extract('year', Expense.added_at) == current_year) + .filter(extract('month', Expense.added_at) == current_month) + .scalar() or 0 + ) + + return render_template( + 'admin/admin_panel.html', + user_count=user_count, + list_count=list_count, + item_count=item_count, + purchased_items_count=purchased_items_count, + enriched_lists=enriched_lists, + top_products=top_products, + total_expense_sum=total_expense_sum, + year_expense_sum=year_expense_sum, + month_expense_sum=month_expense_sum, + ) - return render_template( - 'admin/admin_panel.html', - user_count=user_count, - list_count=list_count, - item_count=item_count, - purchased_items_count=purchased_items_count, - enriched_lists=enriched_lists, - top_products=top_products, - ) @app.route('/admin/delete_list/') @login_required @@ -491,15 +547,46 @@ def delete_receipt(filename): def edit_list(list_id): if not current_user.is_admin: return redirect(url_for('index_guest')) + l = ShoppingList.query.get_or_404(list_id) + expenses = Expense.query.filter_by(list_id=list_id).all() + total_expense = sum(e.amount for e in expenses) + if request.method == 'POST': new_title = request.form.get('title') - if new_title: - l.title = new_title + new_amount_str = request.form.get('amount') + is_archived = 'archived' in request.form + + if new_title and new_title.strip(): + l.title = new_title.strip() + + l.is_archived = is_archived + + if new_amount_str: + try: + new_amount = float(new_amount_str) + + + if expenses: + for expense in expenses: + db.session.delete(expense) + db.session.commit() + + new_expense = Expense(list_id=list_id, amount=new_amount) + db.session.add(new_expense) + db.session.commit() + flash('Zaktualizowano tytuł, archiwizację i/lub kwotę wydatku', 'success') + except ValueError: + flash('Niepoprawna kwota', 'danger') + return redirect(url_for('edit_list', list_id=list_id)) + else: db.session.commit() - flash('Zaktualizowano tytuł listy', 'success') + flash('Zaktualizowano tytuł i/lub archiwizację', 'success') + return redirect(url_for('admin_panel')) - return render_template('admin/edit_list.html', list=l) + + return render_template('admin/edit_list.html', list=l, total_expense=total_expense) + @app.route('/admin/delete_selected_lists', methods=['POST']) @login_required @@ -537,6 +624,11 @@ def delete_all_items(): flash('Usunięto wszystkie produkty', 'success') return redirect(url_for('admin_panel')) + +# ========================================================================================= +# SOCKET.IO +# ========================================================================================= + @socketio.on('delete_item') def handle_delete_item(data): item = Item.query.get(data['item_id']) @@ -627,6 +719,23 @@ def handle_update_note(data): db.session.commit() emit('note_updated', {'item_id': item_id, 'note': note}, to=str(item.list_id)) +@socketio.on('add_expense') +def handle_add_expense(data): + list_id = data['list_id'] + amount = data['amount'] + + new_expense = Expense(list_id=list_id, amount=amount) + db.session.add(new_expense) + db.session.commit() + + total = db.session.query(func.sum(Expense.amount)).filter_by(list_id=list_id).scalar() or 0 + + emit('expense_added', { + 'amount': amount, + 'total': total + }, to=str(list_id)) + + @app.cli.command('create_db') def create_db(): db.create_all() diff --git a/static/js/live.js b/static/js/live.js index 97429e3..7f6bac4 100644 --- a/static/js/live.js +++ b/static/js/live.js @@ -91,6 +91,24 @@ function setupList(listId, username) { updateItemState(data.item_id, false); }); + socket.on('expense_added', data => { + // Osobne bo w html musi byc unikatowy a wyswietlam w dwoch miejscach + const badgeEl = document.getElementById('total-expense1'); + if (badgeEl) { + badgeEl.innerHTML = `💸 ${data.total.toFixed(2)} PLN`; + badgeEl.classList.remove('bg-secondary'); + badgeEl.classList.add('bg-success'); + badgeEl.style.display = ''; + } + + const summaryEl = document.getElementById('total-expense2'); + if (summaryEl) { + summaryEl.innerHTML = `💸 Łącznie wydano: ${data.total.toFixed(2)} PLN`; + } + + showToast(`Dodano wydatek: ${data.amount.toFixed(2)} PLN`); + }); + socket.on('item_added', data => { showToast(`${data.added_by} dodał: ${data.name}`); const li = document.createElement('li'); @@ -226,6 +244,21 @@ function editItem(id, oldName) { } } +function submitExpense() { + const amountInput = document.getElementById('expenseAmount'); + const amount = parseFloat(amountInput.value); + if (isNaN(amount) || amount <= 0) { + showToast('Podaj poprawną kwotę!'); + return; + } + socket.emit('add_expense', { + list_id: LIST_ID, + amount: amount + }); + amountInput.value = ''; +} + + function copyLink(link) { if (navigator.share) { navigator.share({ diff --git a/templates/admin/admin_panel.html b/templates/admin/admin_panel.html index dceb4f8..bfb0ee2 100644 --- a/templates/admin/admin_panel.html +++ b/templates/admin/admin_panel.html @@ -7,35 +7,81 @@ ← Powrót do strony głównej - + + + +
+
+
+
+

👤 Liczba użytkowników: {{ user_count }}

+

📝 Liczba list zakupowych: {{ list_count }}

+

🛒 Liczba produktów: {{ item_count }}

+

✅ Zakupionych produktów: {{ purchased_items_count }}

+
+
+
+ + {% if top_products %} +
+
+
+
🔥 Najczęściej kupowane produkty:
+
    + {% for name, count in top_products %} +
  • {{ name }} — {{ count }}×
  • + {% endfor %} +
+
+
+
+ {% endif %} + +
+
+
+
💸 Podsumowanie wydatków:
+
    +
  • Obecny miesiąc: {{ '%.2f'|format(month_expense_sum) }} PLN
  • +
  • Obecny rok: {{ '%.2f'|format(year_expense_sum) }} PLN
  • +
  • Całkowite: {{ '%.2f'|format(total_expense_sum) }} PLN
  • +
+
+
-{% if top_products %} -
-
-
🔥 Najczęściej kupowane produkty:
-
    - {% for name, count in top_products %} -
  • {{ name }} — {{ count }}×
  • - {% endfor %} -
-
-
-{% endif %} +

📄 Wszystkie listy zakupowe

@@ -52,6 +98,7 @@ Wypełnienie Komentarze Paragony + Wydatki Akcje @@ -76,14 +123,22 @@ {{ e.purchased_count }}/{{ e.total_count }} ({{ e.percent }}%) {{ e.comments_count }} {{ e.receipts_count }} + + {% if e.total_expense > 0 %} + {{ '%.2f'|format(e.total_expense) }} PLN + {% else %} + - + {% endif %} + - ✏️ Edytuj + ✏️ Edytuj liste 📥 Archiwizuj 🗑️ Usuń {% endfor %} + diff --git a/templates/admin/edit_list.html b/templates/admin/edit_list.html index 8283b28..0e5c1c9 100644 --- a/templates/admin/edit_list.html +++ b/templates/admin/edit_list.html @@ -3,7 +3,7 @@ {% block content %}
-

✏️ Edytuj tytuł listy

+

✏️ Edytuj listę #{{ list.id }}

← Powrót
@@ -12,7 +12,22 @@ + +
+ + + Jeśli nie chcesz zmieniać kwoty, zostaw to pole bez zmian. +
+ +
+ + +
+ + Anuluj
{% endblock %} diff --git a/templates/index.html b/templates/index.html index b9234b6..f15dd31 100644 --- a/templates/index.html +++ b/templates/index.html @@ -44,7 +44,12 @@
-
Produkty: {{ purchased_count }}/{{ total_count }} ({{ percent|round(0) }}%)
+
+ Produkty: {{ purchased_count }}/{{ total_count }} ({{ percent|round(0) }}%) + {% if l.total_expense > 0 %} + — 💸 {{ '%.2f'|format(l.total_expense) }} PLN + {% endif %} +
{% endfor %} diff --git a/templates/list.html b/templates/list.html index ef5082f..e4d5d08 100644 --- a/templates/list.html +++ b/templates/list.html @@ -3,7 +3,16 @@ {% block content %}
-

Lista: {{ list.title }}

+ +

+ Lista: {{ list.title }} + {% if list.is_archived %} + (Archiwalna) + {% endif %} +

+ + + ← Powrót do list
@@ -15,9 +24,15 @@ {{ request.url_root }}share/{{ list.share_token }} - + {% if not list.is_archived %} + + {% else %} + + {% endif %} @@ -25,7 +40,8 @@
📊 Postęp listy — {{ purchased_count }}/{{ total_count }} kupionych ({{ percent|round(0) }}%)
-
+ +
@@ -33,30 +49,49 @@
+{% if total_expense > 0 %} +
+ 💸 Łącznie wydano: {{ '%.2f'|format(total_expense) }} PLN +
+{% else %} + +{% endif %} +
    {% for item in items %}
  • - + {{ item.name }} {% if item.note %} [ Notatka: {{ item.note }} ] {% endif %}
    - - + + + +
  • {% endfor %}
- -
- - -
+{% if not list.is_archived %} +
+ + +
+{% endif %} {% set receipt_pattern = 'list_' ~ list.id %} {% if receipt_files %} diff --git a/templates/list_guest.html b/templates/list_guest.html index dd35992..0002076 100644 --- a/templates/list_guest.html +++ b/templates/list_guest.html @@ -2,31 +2,58 @@ {% block title %}Lista: {{ list.title }}{% endblock %} {% block content %} -
-

🛍️ {{ list.title }} (Gość)

-
+

+ 🛍️ {{ list.title }} + {% if list.is_archived %} + (Archiwalna) + {% endif %} + {% if total_expense > 0 %} + + 💸 {{ '%.2f'|format(total_expense) }} PLN + + {% else %} + + {% endif %} +

    {% for item in items %}
  • - + {{ item.name }} {% if item.note %} [ Notatka: {{ item.note }} ] {% endif %}
    - +
  • {% endfor %}
-
- - -
+{% if not list.is_archived %} +
+ + +
+{% endif %} + + +{% if not list.is_archived %} +
+
💰 Dodaj wydatek
+
+ + +
+

💸 Łącznie wydano: {{ '%.2f'|format(total_expense) }} PLN

+{% endif %} -
{% set receipt_pattern = 'list_' ~ list.id %} {% if receipt_files %} @@ -46,14 +73,16 @@

Brak wgranych paragonów do tej listy.

{% endif %} -
-
📤 Dodaj zdjęcie paragonu
-
-
- - -
-
+{% if not list.is_archived %} +
+
📤 Dodaj zdjęcie paragonu
+
+
+ + +
+
+{% endif %}
+
+