From e4322f2bc6b7a3361900e84f1d04f4820d19abd0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Fri, 25 Jul 2025 18:27:58 +0200 Subject: [PATCH] nowe funkcje i foxy --- app.py | 83 ++++++++++++++++++++------- static/css/style.css | 9 ++- static/js/functions.js | 7 +-- static/js/receipt_crop.js | 58 +++++++++++++++++-- static/js/select_month.js | 14 +++++ static/js/sort_mode.js | 105 +++++++++++++++++++--------------- static/js/user_expenses.js | 6 +- templates/admin/receipts.html | 5 ++ templates/main.html | 55 +++++++++++++++++- templates/user_expenses.html | 12 ++-- 10 files changed, 267 insertions(+), 87 deletions(-) create mode 100644 static/js/select_month.js diff --git a/app.py b/app.py index 6014b47..907bab3 100644 --- a/app.py +++ b/app.py @@ -295,7 +295,9 @@ def save_resized_image(file, path): image.info.clear() new_path = path.rsplit(".", 1)[0] + ".webp" - image.save(new_path, format="WEBP", quality=100, method=0) + # image.save(new_path, format="WEBP", quality=100, method=0) + image.save(new_path, format="WEBP", lossless=True, method=6) + except Exception as e: raise ValueError(f"Błąd podczas przetwarzania obrazu: {e}") @@ -662,13 +664,34 @@ def favicon(): @app.route("/") def main_page(): - # now = datetime.utcnow() now = datetime.now(timezone.utc) + month_str = request.args.get("month") + start = end = None + + if month_str: + try: + year, month = map(int, month_str.split("-")) + start = datetime(year, month, 1, tzinfo=timezone.utc) + end = (start + timedelta(days=31)).replace(day=1) + except: + start = end = None + + def date_filter(query): + if start and end: + query = query.filter( + ShoppingList.created_at >= start, ShoppingList.created_at < end + ) + return query if current_user.is_authenticated: user_lists = ( - ShoppingList.query.filter_by(owner_id=current_user.id, is_archived=False) - .filter((ShoppingList.expires_at == None) | (ShoppingList.expires_at > now)) + date_filter( + 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() ) @@ -680,11 +703,16 @@ def main_page(): ) 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, + date_filter( + 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() @@ -693,10 +721,15 @@ def main_page(): user_lists = [] archived_lists = [] public_lists = ( - ShoppingList.query.filter( - ShoppingList.is_public == True, - ((ShoppingList.expires_at == None) | (ShoppingList.expires_at > now)), - ShoppingList.is_archived == False, + date_filter( + 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() @@ -710,6 +743,8 @@ def main_page(): user_lists=user_lists, public_lists=public_lists, archived_lists=archived_lists, + now=now, + timedelta=timedelta, ) @@ -1028,16 +1063,24 @@ def user_expenses(): ) -@app.route("/user/expenses_data") +@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") + show_all = request.args.get("show_all", "false").lower() == "true" - query = Expense.query.join(ShoppingList, Expense.list_id == ShoppingList.id).filter( - ShoppingList.owner_id == current_user.id - ) + query = Expense.query.join(ShoppingList, Expense.list_id == ShoppingList.id) + + if show_all: + query = query.filter( + or_( + ShoppingList.owner_id == current_user.id, ShoppingList.is_public == True + ) + ) + else: + query = query.filter(ShoppingList.owner_id == current_user.id) if start_date and end_date: try: @@ -2110,15 +2153,15 @@ def crop_receipt(): old_path = os.path.join(app.config["UPLOAD_FOLDER"], receipt.filename) try: - image = Image.open(file).convert("RGB") new_filename = generate_new_receipt_filename(receipt.list_id) new_path = os.path.join(app.config["UPLOAD_FOLDER"], new_filename) - image.save(new_path, format="WEBP", quality=100) + + save_resized_image(file, new_path) if os.path.exists(old_path): os.remove(old_path) - receipt.filename = new_filename + receipt.filename = os.path.basename(new_path) db.session.commit() return jsonify(success=True) diff --git a/static/css/style.css b/static/css/style.css index ee72e62..d7e1dea 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -205,7 +205,6 @@ input.form-control { box-shadow: 0 -1px 4px rgba(0, 0, 0, 0.25); } - @media (max-width: 768px) { .info-bar-fixed { position: static; @@ -310,4 +309,12 @@ input.form-control { .only-mobile { display: none !important; } +} + +.sorting-active { + border: 2px dashed #ffc107; + border-radius: 0.5rem; + background-color: rgba(255, 193, 7, 0.05); + padding: 0.5rem; + transition: border 0.3s, background-color 0.3s; } \ No newline at end of file diff --git a/static/js/functions.js b/static/js/functions.js index fcabe77..1154a86 100644 --- a/static/js/functions.js +++ b/static/js/functions.js @@ -272,7 +272,6 @@ function isListDifferent(oldItems, newItems) { return false; } - function renderItem(item, isShare = window.IS_SHARE, showEditOnly = false) { const li = document.createElement('li'); li.id = `item-${item.id}`; @@ -297,9 +296,11 @@ function renderItem(item, isShare = window.IS_SHARE, showEditOnly = false) { let reasonHTML = item.not_purchased_reason ? `[ Powód: ${item.not_purchased_reason} ]` : ''; + let dragHandle = window.isSorting ? `` : ''; + let left = `
- ${window.isSorting ? `` : ''} + ${dragHandle} ${checkboxOrIcon} ${item.name} ${quantityBadge} ${noteHTML} @@ -361,8 +362,6 @@ function renderItem(item, isShare = window.IS_SHARE, showEditOnly = false) { return li; } - - function updateListSmoothly(newItems) { const itemsContainer = document.getElementById('items'); const existingItemsMap = new Map(); diff --git a/static/js/receipt_crop.js b/static/js/receipt_crop.js index 94bf38d..33754a0 100644 --- a/static/js/receipt_crop.js +++ b/static/js/receipt_crop.js @@ -4,22 +4,22 @@ let currentReceiptId; document.addEventListener("DOMContentLoaded", function () { const cropModal = document.getElementById("cropModal"); const cropImage = document.getElementById("cropImage"); + const spinner = document.getElementById("cropLoading"); cropModal.addEventListener("shown.bs.modal", function (event) { const button = event.relatedTarget; const imgSrc = button.getAttribute("data-img-src"); currentReceiptId = button.getAttribute("data-receipt-id"); - const image = document.getElementById("cropImage"); - image.src = imgSrc; + cropImage.src = imgSrc; if (cropper) { cropper.destroy(); cropper = null; } - image.onload = () => { - cropper = new Cropper(image, { + cropImage.onload = () => { + cropper = new Cropper(cropImage, { viewMode: 1, autoCropArea: 1, responsive: true, @@ -36,7 +36,51 @@ document.addEventListener("DOMContentLoaded", function () { document.getElementById("saveCrop").addEventListener("click", function () { if (!cropper) return; - cropper.getCroppedCanvas().toBlob(function (blob) { + spinner.classList.remove("d-none"); + + const cropData = cropper.getData(); + const imageData = cropper.getImageData(); + + const scaleX = imageData.naturalWidth / imageData.width; + const scaleY = imageData.naturalHeight / imageData.height; + + const width = cropData.width * scaleX; + const height = cropData.height * scaleY; + + if (width < 1 || height < 1) { + spinner.classList.add("d-none"); + showToast("Obszar przycięcia jest zbyt mały lub pusty", "danger"); + return; + } + + // Ogranicz do 2000x2000 w proporcji + const maxDim = 2000; + const scale = Math.min(1, maxDim / Math.max(width, height)); + + const finalWidth = Math.round(width * scale); + const finalHeight = Math.round(height * scale); + + const croppedCanvas = cropper.getCroppedCanvas({ + width: finalWidth, + height: finalHeight, + imageSmoothingEnabled: true, + imageSmoothingQuality: 'high', + }); + + + if (!croppedCanvas) { + spinner.classList.add("d-none"); + showToast("Nie można uzyskać obrazu przycięcia", "danger"); + return; + } + + croppedCanvas.toBlob(function (blob) { + if (!blob) { + spinner.classList.add("d-none"); + showToast("Nie udało się zapisać obrazu", "danger"); + return; + } + const formData = new FormData(); formData.append("receipt_id", currentReceiptId); formData.append("cropped_image", blob); @@ -47,6 +91,7 @@ document.addEventListener("DOMContentLoaded", function () { }) .then((res) => res.json()) .then((data) => { + spinner.classList.add("d-none"); if (data.success) { showToast("Zapisano przycięty paragon", "success"); setTimeout(() => location.reload(), 1500); @@ -55,9 +100,10 @@ document.addEventListener("DOMContentLoaded", function () { } }) .catch((err) => { + spinner.classList.add("d-none"); showToast("Błąd sieci", "danger"); console.error(err); }); - }, "image/webp"); + }, "image/webp", 1.0); }); }); diff --git a/static/js/select_month.js b/static/js/select_month.js new file mode 100644 index 0000000..8a7f958 --- /dev/null +++ b/static/js/select_month.js @@ -0,0 +1,14 @@ +document.addEventListener("DOMContentLoaded", () => { + const select = document.getElementById("monthSelect"); + if (!select) return; + select.addEventListener("change", () => { + const month = select.value; + const url = new URL(window.location.href); + if (month) { + url.searchParams.set("month", month); + } else { + url.searchParams.delete("month"); + } + window.location.href = url.toString(); + }); +}); \ No newline at end of file diff --git a/static/js/sort_mode.js b/static/js/sort_mode.js index 0a91843..fad6e84 100644 --- a/static/js/sort_mode.js +++ b/static/js/sort_mode.js @@ -2,53 +2,54 @@ let sortable = null; let isSorting = false; function enableSortMode() { - if (sortable || isSorting) return; + if (isSorting) return; isSorting = true; + window.isSorting = true; localStorage.setItem('sortModeEnabled', 'true'); const itemsContainer = document.getElementById('items'); const listId = window.LIST_ID; - if (!itemsContainer || !listId) return; - sortable = Sortable.create(itemsContainer, { - animation: 150, - handle: '.drag-handle', - ghostClass: 'drag-ghost', - filter: 'input, button', - preventOnFilter: false, - onEnd: function () { - const order = Array.from(itemsContainer.children) - .map(li => parseInt(li.id.replace('item-', ''))) - .filter(id => !isNaN(id)); - - fetch('/reorder_items', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ list_id: listId, order }) - }).then(() => { - showToast('Zapisano nową kolejność', 'success'); - - if (window.currentItems) { - window.currentItems = order.map(id => - window.currentItems.find(item => item.id === id) - ); - updateListSmoothly(window.currentItems); - } - }); - } - }); - - const btn = document.getElementById('sort-toggle-btn'); - if (btn) { - btn.textContent = '✔️ Zakończ sortowanie'; - btn.classList.remove('btn-outline-warning'); - btn.classList.add('btn-outline-success'); - } - + // Odśwież widok listy z uchwytami (☰) if (window.currentItems) { updateListSmoothly(window.currentItems); } + + // Poczekaj na DOM po odświeżeniu listy + setTimeout(() => { + if (sortable) sortable.destroy(); + + sortable = Sortable.create(itemsContainer, { + animation: 150, + handle: '.drag-handle', + ghostClass: 'drag-ghost', + filter: 'input, button', + preventOnFilter: false, + onEnd: () => { + const order = Array.from(itemsContainer.children) + .map(li => parseInt(li.id.replace('item-', ''))) + .filter(id => !isNaN(id)); + + fetch('/reorder_items', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ list_id: listId, order }) + }).then(() => { + showToast('Zapisano nową kolejność', 'success'); + + if (window.currentItems) { + window.currentItems = order.map(id => + window.currentItems.find(item => item.id === id) + ); + updateListSmoothly(window.currentItems); + } + }); + } + }); + + updateSortButtonUI(true); + }, 50); } function disableSortMode() { @@ -56,28 +57,40 @@ function disableSortMode() { sortable.destroy(); sortable = null; } + isSorting = false; localStorage.removeItem('sortModeEnabled'); - - const btn = document.getElementById('sort-toggle-btn'); - if (btn) { - btn.textContent = '✳️ Zmień kolejność'; - btn.classList.remove('btn-outline-success'); - btn.classList.add('btn-outline-warning'); - } - + window.isSorting = false; if (window.currentItems) { updateListSmoothly(window.currentItems); } + + updateSortButtonUI(false); + } function toggleSortMode() { isSorting ? disableSortMode() : enableSortMode(); } +function updateSortButtonUI(active) { + const btn = document.getElementById('sort-toggle-btn'); + if (!btn) return; + + if (active) { + btn.textContent = '✔️ Zakończ sortowanie'; + btn.classList.remove('btn-outline-warning'); + btn.classList.add('btn-outline-success'); + } else { + btn.textContent = '✳️ Zmień kolejność'; + btn.classList.remove('btn-outline-success'); + btn.classList.add('btn-outline-warning'); + } +} + document.addEventListener('DOMContentLoaded', () => { const wasSorting = localStorage.getItem('sortModeEnabled') === 'true'; if (wasSorting) { enableSortMode(); } -}); \ No newline at end of file +}); diff --git a/static/js/user_expenses.js b/static/js/user_expenses.js index a029e70..437dcfa 100644 --- a/static/js/user_expenses.js +++ b/static/js/user_expenses.js @@ -3,7 +3,11 @@ document.addEventListener("DOMContentLoaded", function () { const rangeLabel = document.getElementById("chartRangeLabel"); function loadExpenses(range = "monthly", startDate = null, endDate = null) { - let url = '/user/expenses_data?range=' + range; + let url = '/user_expenses_data?range=' + range; + const showAllCheckbox = document.getElementById("showAllLists"); + if (showAllCheckbox && showAllCheckbox.checked) { + url += '&show_all=true'; + } if (startDate && endDate) { url += `&start_date=${startDate}&end_date=${endDate}`; } diff --git a/templates/admin/receipts.html b/templates/admin/receipts.html index 736a512..3361b37 100644 --- a/templates/admin/receipts.html +++ b/templates/admin/receipts.html @@ -78,6 +78,11 @@
diff --git a/templates/main.html b/templates/main.html index 1891bc7..1cce537 100644 --- a/templates/main.html +++ b/templates/main.html @@ -31,6 +31,33 @@ {% endif %} +{% set month_names = ["styczeń", "luty", "marzec", "kwiecień", "maj", "czerwiec", "lipiec", "sierpień", "wrzesień", +"październik", "listopad", "grudzień"] %} +{% set selected_month = request.args.get('month') or now.strftime('%Y-%m') %} + + +
+ + +
+ + +
+ +
+ {% if current_user.is_authenticated %}

Twoje listy @@ -78,8 +105,7 @@ {% endfor %} {% else %} -

Nie masz jeszcze żadnych list. Utwórz pierwszą, korzystając - z formularza powyżej

+

Nie utworzono żadnej listy

{% endif %} {% endif %} @@ -114,7 +140,6 @@

Brak dostępnych list publicznych do wyświetlenia

{% endif %} - - +
+ + +
-
- - -
+
Od