diff --git a/alters.txt b/alters.txt index d0c1059..452566e 100644 --- a/alters.txt +++ b/alters.txt @@ -20,4 +20,10 @@ CREATE TABLE expense ( added_at DATETIME DEFAULT CURRENT_TIMESTAMP, receipt_filename VARCHAR(255), FOREIGN KEY(list_id) REFERENCES shopping_list(id) -); \ No newline at end of file +); + +# FUNKCJA UKRYCIA PUBLICZNIE LISTY +ALTER TABLE shopping_list ADD COLUMN is_public BOOLEAN NOT NULL DEFAULT 1; + +# ilośc produktów +ALTER TABLE item ADD COLUMN quantity INTEGER DEFAULT 1; diff --git a/app.py b/app.py index 20df8ec..fb91ae5 100644 --- a/app.py +++ b/app.py @@ -49,7 +49,7 @@ class ShoppingList(db.Model): expires_at = db.Column(db.DateTime, nullable=True) owner = db.relationship('User', backref='lists', lazy=True) is_archived = db.Column(db.Boolean, default=False) - + is_public = db.Column(db.Boolean, default=True) class Item(db.Model): id = db.Column(db.Integer, primary_key=True) list_id = db.Column(db.Integer, db.ForeignKey('shopping_list.id')) @@ -58,6 +58,7 @@ class Item(db.Model): added_by = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=True) purchased = db.Column(db.Boolean, default=False) purchased_at = db.Column(db.DateTime, nullable=True) + quantity = db.Column(db.Integer, default=1) note = db.Column(db.Text, nullable=True) class SuggestedProduct(db.Model): @@ -166,23 +167,6 @@ def system_auth(): flash('Nieprawidłowe hasło do systemu','danger') return render_template('system_auth.html') -@app.route('/') -def index_guest(): - 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) def page_not_found(e): @@ -197,6 +181,98 @@ def favicon(): ''' return svg, 200, {'Content-Type': 'image/svg+xml'} +@app.route('/') +def index_guest(): + now = datetime.utcnow() + + if current_user.is_authenticated: + # Twoje listy + user_lists = ShoppingList.query.filter_by(owner_id=current_user.id, is_archived=False).filter( + (ShoppingList.expires_at == None) | (ShoppingList.expires_at > now) + ).order_by(ShoppingList.created_at.desc()).all() + + # Publiczne listy innych użytkowników + public_lists = ShoppingList.query.filter( + ShoppingList.is_public == True, + ShoppingList.owner_id != current_user.id, + ((ShoppingList.expires_at == None) | (ShoppingList.expires_at > now)), + ShoppingList.is_archived == False + ).order_by(ShoppingList.created_at.desc()).all() + else: + user_lists = [] + public_lists = ShoppingList.query.filter( + ShoppingList.is_public == True, + ((ShoppingList.expires_at == None) | (ShoppingList.expires_at > now)), + ShoppingList.is_archived == False + ).order_by(ShoppingList.created_at.desc()).all() + + # Liczenie produktów i wydatków + for l in user_lists + public_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", user_lists=user_lists, public_lists=public_lists) + +@app.route('/archive_my_list/') +@login_required +def archive_my_list(list_id): + l = ShoppingList.query.get_or_404(list_id) + if l.owner_id != current_user.id: + flash('Nie masz uprawnień do tej listy', 'danger') + return redirect(url_for('index_guest')) + l.is_archived = True + db.session.commit() + flash('Lista została zarchiwizowana', 'success') + return redirect(url_for('index_guest')) + +@app.route('/edit_my_list/', methods=['GET', 'POST']) +@login_required +def edit_my_list(list_id): + l = ShoppingList.query.get_or_404(list_id) + if l.owner_id != current_user.id: + flash('Nie masz uprawnień do tej listy', 'danger') + return redirect(url_for('index_guest')) + + if request.method == 'POST': + new_title = request.form.get('title') + if new_title and new_title.strip(): + l.title = new_title.strip() + db.session.commit() + flash('Zaktualizowano tytuł listy', 'success') + return redirect(url_for('index_guest')) + else: + flash('Podaj poprawny tytuł', 'danger') + return render_template('edit_my_list.html', list=l) + +@app.route('/toggle_visibility/', methods=['GET', 'POST']) +@login_required +def toggle_visibility(list_id): + l = ShoppingList.query.get_or_404(list_id) + if l.owner_id != current_user.id: + if request.is_json or request.method == 'POST': + return {'error': 'Unauthorized'}, 403 + flash('Nie masz uprawnień do tej listy', 'danger') + return redirect(url_for('index_guest')) + + l.is_public = not l.is_public + db.session.commit() + + share_url = f"{request.url_root}share/{l.share_token}" + + if request.is_json or request.method == 'POST': + return {'is_public': l.is_public, 'share_url': share_url} + + if l.is_public: + flash('Lista została udostępniona publicznie', 'success') + else: + flash('Lista została ukryta przed gośćmi', 'info') + + return redirect(url_for('index_guest')) + + @app.route('/login', methods=['GET', 'POST']) def login(): if request.method == 'POST': @@ -213,7 +289,7 @@ def login(): def logout(): logout_user() flash('Wylogowano pomyślnie', 'success') - return redirect(url_for('login')) + return redirect(url_for('index_guest')) @app.route('/create', methods=['POST']) @login_required @@ -257,6 +333,11 @@ def view_list(list_id): @app.route('/share/') def share_list(token): shopping_list = ShoppingList.query.filter_by(share_token=token).first_or_404() + + if not shopping_list.is_public: + flash('Ta lista nie jest publicznie dostępna', 'danger') + return redirect(url_for('index_guest')) + items = Item.query.filter_by(list_id=shopping_list.id).all() receipt_pattern = f"list_{shopping_list.id}" @@ -275,7 +356,6 @@ def share_list(token): total_expense=total_expense ) - @app.route('/guest-list/') def guest_list(list_id): shopping_list = ShoppingList.query.get_or_404(list_id) @@ -542,51 +622,6 @@ def delete_receipt(filename): flash('Plik nie istnieje', 'danger') return redirect(url_for('admin_receipts')) -@app.route('/admin/edit_list/', methods=['GET', 'POST']) -@login_required -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') - 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ł i/lub archiwizację', 'success') - - return redirect(url_for('admin_panel')) - - return render_template('admin/edit_list.html', list=l, total_expense=total_expense) - @app.route('/admin/delete_selected_lists', methods=['POST']) @login_required @@ -625,6 +660,66 @@ def delete_all_items(): return redirect(url_for('admin_panel')) +@app.route('/admin/edit_list/', methods=['GET', 'POST']) +@login_required +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) + + users = User.query.all() + + if request.method == 'POST': + new_title = request.form.get('title') + new_amount_str = request.form.get('amount') + is_archived = 'archived' in request.form + new_owner_id = request.form.get('owner_id') + + if new_title and new_title.strip(): + l.title = new_title.strip() + + l.is_archived = is_archived + + if new_owner_id: + try: + new_owner_id_int = int(new_owner_id) + if User.query.get(new_owner_id_int): + l.owner_id = new_owner_id_int + else: + flash('Wybrany użytkownik nie istnieje', 'danger') + return redirect(url_for('edit_list', list_id=list_id)) + except ValueError: + flash('Niepoprawny ID użytkownika', 'danger') + return redirect(url_for('edit_list', list_id=list_id)) + + 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ł, właściciela, 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ł, właściciela i/lub archiwizację', 'success') + + return redirect(url_for('admin_panel')) + + return render_template('admin/edit_list.html', list=l, total_expense=total_expense, users=users) + + # ========================================================================================= # SOCKET.IO # ========================================================================================= @@ -641,10 +736,28 @@ def handle_delete_item(data): def handle_edit_item(data): item = Item.query.get(data['item_id']) new_name = data['new_name'] + new_quantity = data.get('new_quantity', item.quantity) + if item and new_name.strip(): - item.name = new_name + item.name = new_name.strip() + + try: + new_quantity = int(new_quantity) + if new_quantity < 1: + new_quantity = 1 + except: + new_quantity = 1 + + item.quantity = new_quantity + db.session.commit() - emit('item_edited', {'item_id': item.id, 'new_name': item.name}, to=str(item.list_id)) + + emit('item_edited', { + 'item_id': item.id, + 'new_name': item.name, + 'new_quantity': item.quantity + }, to=str(item.list_id)) + @socketio.on('join_list') def handle_join(data): @@ -657,9 +770,19 @@ def handle_join(data): def handle_add_item(data): list_id = data['list_id'] name = data['name'] + quantity = data.get('quantity', 1) + + try: + quantity = int(quantity) + if quantity < 1: + quantity = 1 + except: + quantity = 1 + new_item = Item( list_id=list_id, name=name, + quantity=quantity, added_by=current_user.id if current_user.is_authenticated else None ) db.session.add(new_item) @@ -669,9 +792,11 @@ def handle_add_item(data): db.session.add(new_suggestion) db.session.commit() + emit('item_added', { 'id': new_item.id, 'name': new_item.name, + 'quantity': new_item.quantity, 'added_by': current_user.username if current_user.is_authenticated else 'Gość' }, to=str(list_id), include_self=True) diff --git a/static/js/hide_list.js b/static/js/hide_list.js new file mode 100644 index 0000000..e806293 --- /dev/null +++ b/static/js/hide_list.js @@ -0,0 +1,29 @@ +function toggleVisibility(listId) { + fetch('/toggle_visibility/' + listId, {method: 'POST'}) + .then(response => response.json()) + .then(data => { + const shareHeader = document.getElementById('share-header'); + const shareUrlSpan = document.getElementById('share-url'); + const copyBtn = document.getElementById('copyBtn'); + const toggleBtn = document.getElementById('toggleVisibilityBtn'); + + if (data.is_public) { + shareHeader.textContent = '🔗 Udostępnij link:'; + shareUrlSpan.style.display = 'inline'; + shareUrlSpan.textContent = data.share_url; + copyBtn.disabled = false; + toggleBtn.innerHTML = '🙈 Ukryj listę'; + } else { + shareHeader.textContent = '🙈 Lista jest ukryta przed gośćmi'; + shareUrlSpan.style.display = 'none'; + copyBtn.disabled = true; + toggleBtn.innerHTML = '👁️ Udostępnij ponownie'; + } + }); +} + +function copyLink(link) { + navigator.clipboard.writeText(link).then(() => { + alert('Link skopiowany!'); + }); +} \ No newline at end of file diff --git a/static/js/live.js b/static/js/live.js index 7f6bac4..c3b5168 100644 --- a/static/js/live.js +++ b/static/js/live.js @@ -93,6 +93,7 @@ function setupList(listId, username) { 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`; @@ -114,16 +115,23 @@ function setupList(listId, username) { const li = document.createElement('li'); li.className = 'list-group-item d-flex justify-content-between align-items-center flex-wrap bg-light text-dark'; li.id = `item-${data.id}`; + + let quantityBadge = ''; + if (data.quantity && data.quantity > 1) { + quantityBadge = `x${data.quantity}`; + } + li.innerHTML = `
- ${data.name} + ${data.name} ${quantityBadge}
- +
`; + document.getElementById('items').appendChild(li); updateProgressBar(); }); @@ -181,9 +189,13 @@ function setupList(listId, username) { socket.on('item_edited', data => { const nameSpan = document.getElementById(`name-${data.item_id}`); if (nameSpan) { - nameSpan.innerText = data.new_name; + let quantityBadge = ''; + if (data.new_quantity && data.new_quantity > 1) { + quantityBadge = ` x${data.new_quantity}`; + } + nameSpan.innerHTML = `${data.new_name}${quantityBadge}`; } - showToast(`Zmieniono nazwę na: ${data.new_name}`); + showToast(`Zaktualizowano produkt: ${data.new_name} (x${data.new_quantity})`); }); updateProgressBar(); @@ -225,23 +237,47 @@ function updateProgressBar() { function addItem(listId) { const name = document.getElementById('newItem').value; + const quantityInput = document.getElementById('newQuantity'); + let quantity = 1; + + if (quantityInput) { + quantity = parseInt(quantityInput.value); + if (isNaN(quantity) || quantity < 1) { + quantity = 1; + } + } + if (name.trim() === '') return; - socket.emit('add_item', { list_id: listId, name: name }); + + socket.emit('add_item', { list_id: listId, name: name, quantity: quantity }); + document.getElementById('newItem').value = ''; + if (quantityInput) quantityInput.value = 1; document.getElementById('newItem').focus(); } + function deleteItem(id) { if (confirm('Na pewno usunąć produkt?')) { socket.emit('delete_item', { item_id: id }); } } -function editItem(id, oldName) { - const newName = prompt('Podaj nową nazwę:', oldName); - if (newName && newName.trim() !== '') { - socket.emit('edit_item', { item_id: id, new_name: newName }); +function editItem(id, oldName, oldQuantity) { + const newName = prompt('Podaj nową nazwę (lub zostaw starą):', oldName); + if (newName === null) return; // anulował + + const newQuantityStr = prompt('Podaj nową ilość:', oldQuantity); + if (newQuantityStr === null) return; // anulował + + const finalName = newName.trim() !== '' ? newName.trim() : oldName; + + let newQuantity = parseInt(newQuantityStr); + if (isNaN(newQuantity) || newQuantity < 1) { + newQuantity = oldQuantity; } + + socket.emit('edit_item', { item_id: id, new_name: finalName, new_quantity: newQuantity }); } function submitExpense() { diff --git a/templates/admin/admin_panel.html b/templates/admin/admin_panel.html index bfb0ee2..b17ce83 100644 --- a/templates/admin/admin_panel.html +++ b/templates/admin/admin_panel.html @@ -92,6 +92,7 @@ ID Tytuł + Status Utworzono Właściciel Produkty @@ -111,6 +112,15 @@ {{ l.title }} + + {% if l.is_archived %} + Archiwalna + {% elif l.is_temporary and l.expires_at and l.expires_at < now %} + Wygasła + {% else %} + Aktywna + {% endif %} + {{ l.created_at.strftime('%Y-%m-%d %H:%M') if l.created_at else '-' }} {% if l.owner_id %} diff --git a/templates/admin/edit_list.html b/templates/admin/edit_list.html index 0e5c1c9..1c096e4 100644 --- a/templates/admin/edit_list.html +++ b/templates/admin/edit_list.html @@ -19,6 +19,17 @@ Jeśli nie chcesz zmieniać kwoty, zostaw to pole bez zmian. +
+ + +
+