diff --git a/app.py b/app.py index 12015df..65ccefb 100644 --- a/app.py +++ b/app.py @@ -35,20 +35,6 @@ UPLOAD_FOLDER = app.config.get('UPLOAD_FOLDER', 'uploads') ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif', 'webp'} AUTHORIZED_COOKIE_VALUE = app.config.get('AUTHORIZED_COOKIE_VALUE', '80d31cdfe63539c9') -PROTECTED_JS_FILES = { - "live.js", - "notes.js", - "sockets.js", - "product_suggestion.js", - "expenses.js", - "toggle_button.js", - "user_management.js", - "mass_add.js", - "functions.js", - "clickable_row.js", - "receipt_section.js" -} - os.makedirs(UPLOAD_FOLDER, exist_ok=True) failed_login_attempts = defaultdict(deque) @@ -273,7 +259,7 @@ def require_system_password(): if request.endpoint == 'static_bp.serve_js': requested_file = request.view_args.get("filename", "") - if requested_file in PROTECTED_JS_FILES: + if requested_file.endswith(".js"): return redirect(url_for('system_auth', next=request.url)) else: return @@ -596,7 +582,7 @@ def all_products(): return {'allproducts': all_names} -@app.route('/upload_receipt/', methods=['POST']) +""" @app.route('/upload_receipt/', methods=['POST']) def upload_receipt(list_id): if 'receipt' not in request.files: flash('Brak pliku', 'danger') @@ -618,6 +604,41 @@ def upload_receipt(list_id): return redirect(request.referrer) flash('Niedozwolony format pliku', 'danger') + return redirect(request.referrer) """ + +@app.route('/upload_receipt/', methods=['POST']) +def upload_receipt(list_id): + if 'receipt' not in request.files: + if request.is_json or request.headers.get('X-Requested-With') == 'XMLHttpRequest': + return jsonify({'success': False, 'message': 'Brak pliku'}), 400 + flash('Brak pliku', 'danger') + return redirect(request.referrer) + + file = request.files['receipt'] + + if file.filename == '': + if request.is_json or request.headers.get('X-Requested-With') == 'XMLHttpRequest': + return jsonify({'success': False, 'message': 'Nie wybrano pliku'}), 400 + flash('Nie wybrano pliku', 'danger') + return redirect(request.referrer) + + if file and allowed_file(file.filename): + filename = secure_filename(file.filename) + full_filename = f"list_{list_id}_{filename}" + file_path = os.path.join(app.config['UPLOAD_FOLDER'], full_filename) + + save_resized_image(file, file_path) + + if request.is_json or request.headers.get('X-Requested-With') == 'XMLHttpRequest': + url = url_for('uploaded_file', filename=full_filename) + return jsonify({'success': True, 'url': url}) + + flash('Wgrano paragon', 'success') + return redirect(request.referrer) + + if request.is_json or request.headers.get('X-Requested-With') == 'XMLHttpRequest': + return jsonify({'success': False, 'message': 'Niedozwolony format pliku'}), 400 + flash('Niedozwolony format pliku', 'danger') return redirect(request.referrer) @app.route('/uploads/') @@ -1096,10 +1117,19 @@ def demote_user(user_id): def handle_delete_item(data): item = Item.query.get(data['item_id']) if item: + list_id = item.list_id db.session.delete(item) db.session.commit() emit('item_deleted', {'item_id': item.id}, to=str(item.list_id)) + purchased_count, total_count, percent = get_progress(list_id) + + emit('progress_updated', { + 'purchased_count': purchased_count, + 'total_count': total_count, + 'percent': percent + }, to=str(list_id)) + @socketio.on('edit_item') def handle_edit_item(data): item = Item.query.get(data['item_id']) @@ -1188,6 +1218,14 @@ def handle_add_item(data): 'added_by': current_user.username if current_user.is_authenticated else 'Gość' }, to=str(list_id), include_self=True) + purchased_count, total_count, percent = get_progress(list_id) + + emit('progress_updated', { + 'purchased_count': purchased_count, + 'total_count': total_count, + 'percent': percent + }, to=str(list_id)) + @socketio.on('check_item') def handle_check_item(data): item = Item.query.get(data['item_id']) @@ -1265,6 +1303,15 @@ def handle_add_expense(data): 'total': total }, to=str(list_id)) +@socketio.on('receipt_uploaded') +def handle_receipt_uploaded(data): + list_id = data['list_id'] + url = data['url'] + + emit('receipt_added', { + 'url': url + }, to=str(list_id), include_self=False) + @app.cli.command('create_db') def create_db(): db.create_all() diff --git a/static/css/style.css b/static/css/style.css index cd45b25..fb1088c 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -32,6 +32,16 @@ white-space: nowrap; } +/* rodzic już ma position-relative */ +.progress-label { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + pointer-events: none; /* klikalne przyciski obok paska nie ucierpią */ + white-space: nowrap; +} + .progress-thin { height: 12px; } @@ -250,3 +260,8 @@ input.form-control { align-items: center; justify-content: space-between; } + +#empty-placeholder { + font-style: italic; + pointer-events: none; +} \ No newline at end of file diff --git a/static/js/functions.js b/static/js/functions.js index 1f56f41..0bd07a3 100644 --- a/static/js/functions.js +++ b/static/js/functions.js @@ -281,6 +281,7 @@ function updateListSmoothly(newItems) { itemsContainer.appendChild(fragment); updateProgressBar(); + toggleEmptyPlaceholder(); } document.addEventListener("DOMContentLoaded", function() { diff --git a/static/js/live.js b/static/js/live.js index 4904934..bed3dbb 100644 --- a/static/js/live.js +++ b/static/js/live.js @@ -1,5 +1,25 @@ const socket = io(); +/*──────────────── placeholder pustej listy ────────────────*/ +function toggleEmptyPlaceholder() { + const list = document.getElementById('items'); + if (!list) return; + + // prawdziwe
  • to te z data‑name lub id="item‑…" + const hasRealItems = list.querySelector('li[data-name], li[id^="item-"]') !== null; + const placeholder = document.getElementById('empty-placeholder'); + + if (!hasRealItems && !placeholder) { + const li = document.createElement('li'); + li.id = 'empty-placeholder'; + li.className = 'list-group-item bg-dark text-secondary text-center w-100'; + li.textContent = 'Brak produktów w tej liście.'; + list.appendChild(li); + } else if (hasRealItems && placeholder) { + placeholder.remove(); + } +} + function setupList(listId, username) { socket.emit('join_list', { room: listId, username: username }); @@ -135,6 +155,7 @@ function setupList(listId, username) { document.getElementById('items').appendChild(li); updateProgressBar(); + toggleEmptyPlaceholder(); }); socket.on('item_deleted', data => { @@ -144,6 +165,7 @@ function setupList(listId, username) { } showToast('Usunięto produkt'); updateProgressBar(); + toggleEmptyPlaceholder(); }); socket.on('progress_updated', function(data) { @@ -197,10 +219,10 @@ function setupList(listId, username) { }); updateProgressBar(); + toggleEmptyPlaceholder(); // --- WAŻNE: zapisz dane do reconnect --- window.LIST_ID = listId; window.usernameForReconnect = username; -} - +} \ No newline at end of file diff --git a/static/js/mass_add.js b/static/js/mass_add.js index bcb4f16..0265f35 100644 --- a/static/js/mass_add.js +++ b/static/js/mass_add.js @@ -3,7 +3,6 @@ document.addEventListener('DOMContentLoaded', function () { const productList = document.getElementById('mass-add-list'); modal.addEventListener('show.bs.modal', async function () { - // 🔥 Za każdym razem od nowa budujemy zbiór produktów już na liście let addedProducts = new Set(); document.querySelectorAll('#items li').forEach(li => { if (li.dataset.name) { @@ -91,16 +90,11 @@ document.addEventListener('DOMContentLoaded', function () { productList.appendChild(li); }); - - - - } catch (err) { productList.innerHTML = '
  • Błąd ładowania danych
  • '; } }); - // 🔥 Aktualizacja na żywo po dodaniu socket.on('item_added', data => { document.querySelectorAll('#mass-add-list li').forEach(li => { const itemName = li.firstChild.textContent.trim(); diff --git a/static/js/receipt_upload.js b/static/js/receipt_upload.js new file mode 100644 index 0000000..6d39ccc --- /dev/null +++ b/static/js/receipt_upload.js @@ -0,0 +1,85 @@ +let receiptToastShown = false; + +document.addEventListener("DOMContentLoaded", function () { + const form = document.getElementById("receiptForm"); + const input = document.getElementById("receiptInput"); + const gallery = document.getElementById("receiptGallery"); + const progressContainer = document.getElementById("progressContainer"); + const progressBar = document.getElementById("progressBar"); + + if (!form || !input || !gallery) return; + + form.addEventListener("submit", function (e) { + e.preventDefault(); + + const file = input.files[0]; + if (!file) { + showToast("Nie wybrano pliku!", "warning"); + return; + } + + const formData = new FormData(); + formData.append("receipt", file); + + const xhr = new XMLHttpRequest(); + xhr.open("POST", form.action, true); + xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest"); + + xhr.upload.onprogress = function (e) { + if (e.lengthComputable) { + const percent = Math.round((e.loaded / e.total) * 100); + progressBar.style.width = percent + "%"; + progressBar.textContent = percent + "%"; + } + }; + + xhr.onloadstart = function () { + progressContainer.style.display = "block"; + progressBar.style.width = "0%"; + progressBar.textContent = "0%"; + }; + + xhr.onloadend = function () { + progressContainer.style.display = "none"; + progressBar.style.width = "0%"; + progressBar.textContent = ""; + input.value = ""; + }; + + xhr.onreadystatechange = function () { + if (xhr.readyState === XMLHttpRequest.DONE) { + if (xhr.status === 200) { + const res = JSON.parse(xhr.responseText); + if (res.success && res.url) { + + fetch(window.location.href) + .then(response => response.text()) + .then(html => { + const parser = new DOMParser(); + const doc = parser.parseFromString(html, "text/html"); + const newGallery = doc.getElementById("receiptGallery"); + if (newGallery) { + gallery.innerHTML = newGallery.innerHTML; + + if (!receiptToastShown) { + showToast("Wgrano paragon", "success"); + receiptToastShown = true; + } + socket.emit("receipt_uploaded", { + list_id: LIST_ID, + url: res.url + }); + } + }); + } else { + showToast(res.message || "Błąd podczas wgrywania.", "danger"); + } + } else { + showToast("Błąd serwera. Spróbuj ponownie.", "danger"); + } + } + }; + + xhr.send(formData); + }); +}); diff --git a/static/js/sockets.js b/static/js/sockets.js index 8b4f1e5..6e8e546 100644 --- a/static/js/sockets.js +++ b/static/js/sockets.js @@ -1,3 +1,5 @@ +let didReceiveFirstFullList = false; + // --- Automatyczny reconnect po powrocie do karty/przywróceniu internetu --- function reconnectIfNeeded() { if (!socket.connected) { @@ -75,14 +77,45 @@ socket.on('user_list', function(data) { } }); -socket.on('full_list', function(data) { - const itemsContainer = document.getElementById('items'); - const oldItems = Array.from(itemsContainer.querySelectorAll('li')); +socket.on('receipt_added', function (data) { + const gallery = document.getElementById("receiptGallery"); + if (!gallery) return; - if (isListDifferent(oldItems, data.items)) { - updateListSmoothly(data.items); - showToast('Lista została zaktualizowana', 'info'); - } else { - updateListSmoothly(data.items); + // Usuń placeholder, jeśli istnieje + const alert = gallery.querySelector(".alert"); + if (alert) { + alert.remove(); } + + // Sprawdź, czy już istnieje obraz z tym URL + const existing = Array.from(gallery.querySelectorAll("img")).find(img => img.src === data.url); + if (!existing) { + const col = document.createElement("div"); + col.className = "col-6 col-md-4 col-lg-3 text-center"; + col.innerHTML = ` + + + + `; + gallery.appendChild(col); + } +}); + + +socket.on('full_list', function (data) { + const itemsContainer = document.getElementById('items'); + + const oldItems = Array.from( + itemsContainer.querySelectorAll('li[data-name], li[id^="item-"]') + ); + + const isDifferent = isListDifferent(oldItems, data.items); + + updateListSmoothly(data.items); + toggleEmptyPlaceholder(); + + if (didReceiveFirstFullList && isDifferent) { + showToast('Lista została zaktualizowana', 'info'); + } + didReceiveFirstFullList = true; }); \ No newline at end of file diff --git a/templates/admin/receipts.html b/templates/admin/receipts.html index bcb4a68..73f3b1e 100644 --- a/templates/admin/receipts.html +++ b/templates/admin/receipts.html @@ -33,7 +33,7 @@ {% if not image_files %} -