From 983114575d92c55204b0bcfe493bc8e62863999a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Mon, 21 Jul 2025 15:50:35 +0200 Subject: [PATCH] uprawnienia ocr i uploadu --- app.py | 61 ++++++++++++++++--------------- static/js/clickable_row.js | 58 +++++++++++++++++------------- static/js/live.js | 32 +++++++++++++---- static/js/receipt_analysis.js | 68 ++++++++++++++++++++++++++--------- templates/list_share.html | 18 +++++----- 5 files changed, 154 insertions(+), 83 deletions(-) diff --git a/app.py b/app.py index 904c2d8..92e07ae 100644 --- a/app.py +++ b/app.py @@ -8,9 +8,8 @@ import platform import psutil import secrets import hashlib - import re -import tempfile + from pillow_heif import register_heif_opener @@ -228,17 +227,6 @@ def serve_css_lib(filename): app.register_blueprint(static_bp) - -def user_has_list_access(list_obj, user): - if not user.is_authenticated: - return False - if list_obj.owner_id == user.id: - return True - if db.session.query(SharedList).filter_by(list_id=list_obj.id, user_id=user.id).first(): - return True - return False - - def allowed_file(filename): return "." in filename and filename.rsplit(".", 1)[1].lower() in ALLOWED_EXTENSIONS @@ -376,7 +364,6 @@ def preprocess_image_for_tesseract(pil_image): def extract_total_tesseract(image): - text = pytesseract.image_to_string(image, lang="pol", config="--psm 6") lines = text.splitlines() candidates = [] @@ -415,23 +402,30 @@ def extract_total_tesseract(image): except: continue + # Rozszerzone słowa kluczowe + keywords = r"sum[aąo]?|razem|zapłat[ay]?|sprzedaż|opodatk|należność|do zapłaty" + preferred = [ val for val, line in candidates - if re.search(r"sum[aąo]?|razem|zapłaty", line.lower()) + if re.search(keywords, line.lower()) ] if preferred: max_val = round(max(preferred), 2) return max_val, lines + # Fallback: wybierz największą wartość jeśli jest sensowna if candidates: max_val = round(max([val for val, _ in candidates]), 2) - return max_val, lines + # Jeśli np. większa niż 10 PLN, zakładamy że to może być suma końcowa + if max_val >= 10: + return max_val, lines return 0.0, lines + ############# END OCR ####################### @@ -1050,9 +1044,6 @@ def all_products(): @app.route("/upload_receipt/", methods=["POST"]) @login_required def upload_receipt(list_id): - list_obj = db.session.get(ShoppingList, list_id) - if not list_obj or not user_has_list_access(list_obj, current_user): - return _receipt_error("Gość/niezalogowany nie może wgrywać plików") if "receipt" not in request.files: return _receipt_error("Brak pliku") @@ -1062,8 +1053,6 @@ def upload_receipt(list_id): return _receipt_error("Nie wybrano pliku") if file and allowed_file(file.filename): - import hashlib - file_bytes = file.read() file.seek(0) file_hash = hashlib.sha256(file_bytes).hexdigest() @@ -1147,11 +1136,12 @@ def reorder_items(): @app.route("/lists//analyze", methods=["POST"]) @login_required def analyze_receipts_for_list(list_id): - list_obj = db.session.get(ShoppingList, list_id) - if not list_obj or not user_has_list_access(list_obj, current_user): - return jsonify({"error": "Brak dostępu"}), 403 - receipt_objs = Receipt.query.filter_by(list_id=list_id).all() + existing_expenses = { + e.receipt_filename for e in Expense.query.filter_by(list_id=list_id).all() + if e.receipt_filename + } + results = [] total = 0.0 @@ -1171,13 +1161,18 @@ def analyze_receipts_for_list(list_id): value = 0.0 lines = [] + already_added = receipt.filename in existing_expenses + results.append({ "id": receipt.id, "filename": receipt.filename, "amount": round(value, 2), "debug_text": lines, + "already_added": already_added }) - total += value + + if not already_added: + total += value return jsonify({"results": results, "total": round(total, 2)}) @@ -1483,8 +1478,6 @@ def generate_receipt_hash(receipt_id): flash("Plik nie istnieje", "danger") return redirect(request.referrer) - import hashlib - try: with open(file_path, "rb") as f: file_hash = hashlib.sha256(f.read()).hexdigest() @@ -2174,8 +2167,18 @@ def handle_update_note(data): def handle_add_expense(data): list_id = data["list_id"] amount = data["amount"] + receipt_filename = data.get("receipt_filename") + + if receipt_filename: + existing = Expense.query.filter_by(list_id=list_id, receipt_filename=receipt_filename).first() + if existing: + return + new_expense = Expense( + list_id=list_id, + amount=amount, + receipt_filename=receipt_filename + ) - new_expense = Expense(list_id=list_id, amount=amount) db.session.add(new_expense) db.session.commit() diff --git a/static/js/clickable_row.js b/static/js/clickable_row.js index 955c80c..4ab51af 100644 --- a/static/js/clickable_row.js +++ b/static/js/clickable_row.js @@ -1,31 +1,41 @@ document.addEventListener("DOMContentLoaded", () => { - document.querySelectorAll('.clickable-item').forEach(item => { - item.addEventListener('click', function (e) { - if (!e.target.closest('button') && e.target.tagName.toLowerCase() !== 'input') { - const checkbox = this.querySelector('input[type="checkbox"]'); + const itemsContainer = document.getElementById('items'); + if (!itemsContainer) return; - if (checkbox.disabled) { - return; - } + itemsContainer.addEventListener('click', function (e) { + const row = e.target.closest('.clickable-item'); + if (!row || !itemsContainer.contains(row)) return; - if (checkbox.checked) { - socket.emit('uncheck_item', { item_id: parseInt(this.id.replace('item-', ''), 10) }); - } else { - socket.emit('check_item', { item_id: parseInt(this.id.replace('item-', ''), 10) }); - } + // Ignoruj kliknięcia w przyciski i inputy + if (e.target.closest('button') || e.target.tagName.toLowerCase() === 'input') { + return; + } - checkbox.disabled = true; - this.classList.add('opacity-50'); + const checkbox = row.querySelector('input[type="checkbox"]'); + if (!checkbox || checkbox.disabled) { + return; + } - let existingSpinner = this.querySelector('.spinner-border'); - if (!existingSpinner) { - const spinner = document.createElement('span'); - spinner.className = 'spinner-border spinner-border-sm ms-2'; - spinner.setAttribute('role', 'status'); - spinner.setAttribute('aria-hidden', 'true'); - checkbox.parentElement.appendChild(spinner); - } - } - }); + const itemId = parseInt(row.id.replace('item-', ''), 10); + if (isNaN(itemId)) return; + + if (checkbox.checked) { + socket.emit('uncheck_item', { item_id: itemId }); + } else { + socket.emit('check_item', { item_id: itemId }); + } + + checkbox.disabled = true; + row.classList.add('opacity-50'); + + // Dodaj spinner tylko jeśli nie ma + let existingSpinner = row.querySelector('.spinner-border'); + if (!existingSpinner) { + const spinner = document.createElement('span'); + spinner.className = 'spinner-border spinner-border-sm ms-2'; + spinner.setAttribute('role', 'status'); + spinner.setAttribute('aria-hidden', 'true'); + checkbox.parentElement.appendChild(spinner); + } }); }); diff --git a/static/js/live.js b/static/js/live.js index a7ac78c..e96ebdb 100644 --- a/static/js/live.js +++ b/static/js/live.js @@ -138,12 +138,20 @@ function setupList(listId, username) { quantityBadge = `x${data.quantity}`; } + const countdownId = `countdown-${data.id}`; + const countdownBtn = ` + + `; + li.innerHTML = `
- ${data.name} ${quantityBadge} + + ${data.name} ${quantityBadge} +
+ ${countdownBtn}
`; - // góra listy - //document.getElementById('items').prepend(li); - - // dół listy document.getElementById('items').appendChild(li); toggleEmptyPlaceholder(); + // ⏳ Licznik odliczania + let seconds = 15; + const countdownEl = document.getElementById(countdownId); + const intervalId = setInterval(() => { + seconds--; + if (countdownEl) { + countdownEl.textContent = `${seconds}s`; + } + if (seconds <= 0) { + clearInterval(intervalId); + if (countdownEl) countdownEl.remove(); + } + }, 1000); + + // 🔁 Request listy po 15s setTimeout(() => { if (window.LIST_ID) { socket.emit('request_full_list', { list_id: window.LIST_ID }); } }, 15000); - }); + + socket.on('item_deleted', data => { const li = document.getElementById(`item-${data.item_id}`); if (li) { diff --git a/static/js/receipt_analysis.js b/static/js/receipt_analysis.js index 6210a5a..ce24650 100644 --- a/static/js/receipt_analysis.js +++ b/static/js/receipt_analysis.js @@ -7,32 +7,46 @@ document.addEventListener("DOMContentLoaded", () => { async function analyzeReceipts(listId) { const resultsDiv = document.getElementById("analysisResults"); - resultsDiv.innerHTML = `
⏳ Trwa analiza paragonów...
`; + resultsDiv.innerHTML = ` +
+
+ Trwa analiza paragonów... +
`; - const start = performance.now(); // ⏱ START + const start = performance.now(); try { const res = await fetch(`/lists/${listId}/analyze`, { method: "POST" }); const data = await res.json(); + const duration = ((performance.now() - start) / 1000).toFixed(2); - const duration = ((performance.now() - start) / 1000).toFixed(2); // ⏱ STOP - - let html = `

📊 Łącznie wykryto: ${data.total.toFixed(2)} PLN

`; + let html = `
`; + html += `

📊 Łącznie wykryto: ${data.total.toFixed(2)} PLN

`; html += `

⏱ Czas analizy OCR: ${duration} sek.

`; data.results.forEach((r, i) => { + const disabled = r.already_added ? "disabled" : ""; + const inputStyle = "form-control d-inline-block bg-dark text-white border-light rounded"; + const inputField = ``; + + const button = r.already_added + ? `✅ Dodano` + : ``; + html += ` -
- ${r.filename}: - - -
`; +
+ ${r.filename} + ${inputField} + ${button} +
`; }); + if (data.results.length > 1) { - html += ``; + html += ``; } + html += `
`; resultsDiv.innerHTML = html; window._ocr_results = data.results; @@ -42,22 +56,44 @@ async function analyzeReceipts(listId) { } } - function emitExpense(i) { const r = window._ocr_results[i]; const val = parseFloat(document.getElementById(`amount-${i}`).value); + const btn = document.getElementById(`add-btn-${i}`); + if (!isNaN(val) && val > 0) { socket.emit('add_expense', { list_id: LIST_ID, - amount: val - + amount: val, + receipt_filename: r.filename }); + document.getElementById(`amount-${i}`).disabled = true; + if (btn) { + btn.disabled = true; + btn.classList.remove('btn-outline-success'); + btn.classList.add('btn-success'); + btn.textContent = '✅ Dodano'; + } } } function emitAllExpenses(n) { - for (let i = 0; i < n; i++) { - emitExpense(i); + const btnAll = document.getElementById('addAllBtn'); + if (btnAll) { + btnAll.disabled = true; + btnAll.innerHTML = `Dodawanie...`; } + + for (let i = 0; i < n; i++) { + setTimeout(() => emitExpense(i), i * 150); + } + + setTimeout(() => { + if (btnAll) { + btnAll.innerHTML = '✅ Wszystko dodano'; + btnAll.classList.remove('btn-success'); + btnAll.classList.add('btn-outline-success'); + } + }, n * 150 + 300); } diff --git a/templates/list_share.html b/templates/list_share.html index 6b1e122..495065f 100644 --- a/templates/list_share.html +++ b/templates/list_share.html @@ -5,7 +5,6 @@

🛍️ {{ list.title }} - {% if list.is_archived %} (Archiwalna) {% endif %} @@ -70,8 +69,6 @@ {% endif %} - - {% else %}
  • Brak produktów w tej liście. @@ -81,10 +78,12 @@ {% if not list.is_archived %}
    - + - + min="1" value="1" style="max-width: 90px;" {% if not current_user.is_authenticated %}disabled{% endif %}> +
    {% endif %} @@ -112,9 +111,13 @@
    🧠 Analiza paragonów (OCR)

    System spróbuje automatycznie rozpoznać kwoty z dodanych paragonów.

    + {% if current_user.is_authenticated %} + {% else %} +
    🔒 Tylko zalogowani użytkownicy mogą zlecać analizę OCR paragonów.
    + {% endif %}
    @@ -140,7 +143,7 @@ {% endif %} - {% if not list.is_archived %} + {% if not list.is_archived and current_user.is_authenticated %}
    📤 Dodaj zdjęcie paragonu
    - {% endif %}