From 34205f0e6570dcf2914482ed942bf75d81212ce4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Thu, 24 Jul 2025 23:30:51 +0200 Subject: [PATCH 01/47] commit #1 --- app.py | 77 +++++++++++++--- static/js/functions.js | 155 +++++++++++++++++++------------- static/js/live.js | 96 +++++++++----------- static/js/user_expense_lists.js | 103 +++++++++++++++++++++ templates/list.html | 4 +- templates/user_expenses.html | 91 ++++++++++++++----- 6 files changed, 377 insertions(+), 149 deletions(-) create mode 100644 static/js/user_expense_lists.js diff --git a/app.py b/app.py index d3e57a6..a670afc 100644 --- a/app.py +++ b/app.py @@ -45,7 +45,8 @@ from config import Config from PIL import Image, ExifTags, ImageFilter, ImageOps from werkzeug.utils import secure_filename from werkzeug.middleware.proxy_fix import ProxyFix -from sqlalchemy import func, extract, inspect +from sqlalchemy import func, extract, inspect, or_ +from sqlalchemy.orm import joinedload from collections import defaultdict, deque from functools import wraps @@ -114,7 +115,10 @@ class ShoppingList(db.Model): id = db.Column(db.Integer, primary_key=True) title = db.Column(db.String(150), nullable=False) created_at = db.Column(db.DateTime, default=datetime.utcnow) + owner_id = db.Column(db.Integer, db.ForeignKey("user.id")) + owner = db.relationship("User", backref="lists", foreign_keys=[owner_id]) + is_temporary = db.Column(db.Boolean, default=False) share_token = db.Column(db.String(64), unique=True, nullable=True) # expires_at = db.Column(db.DateTime, nullable=True) @@ -131,6 +135,8 @@ class Item(db.Model): # added_at = db.Column(db.DateTime, default=datetime.utcnow) added_at = db.Column(db.DateTime, default=utcnow) added_by = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=True) + added_by_user = db.relationship("User", backref="added_items", lazy=True, foreign_keys=[added_by]) + purchased = db.Column(db.Boolean, default=False) purchased_at = db.Column(db.DateTime, nullable=True) quantity = db.Column(db.Integer, default=1) @@ -958,8 +964,7 @@ def view_list(list_id): @app.route("/user_expenses") @login_required def user_expenses(): - from sqlalchemy.orm import joinedload - + # Lista wydatków użytkownika expenses = ( Expense.query.join(ShoppingList, Expense.list_id == ShoppingList.id) .options(joinedload(Expense.list)) @@ -968,7 +973,7 @@ def user_expenses(): .all() ) - rows = [ + expense_table = [ { "title": e.list.title if e.list else "Nieznana", "amount": e.amount, @@ -977,7 +982,35 @@ def user_expenses(): for e in expenses ] - return render_template("user_expenses.html", expense_table=rows) + lists = ( + ShoppingList.query + .filter( + or_( + ShoppingList.owner_id == current_user.id, + ShoppingList.is_public == True + ) + ) + .order_by(ShoppingList.created_at.desc()) + .all() + ) + + lists_data = [ + { + "id": l.id, + "title": l.title, + "created_at": l.created_at, + "total_expense": sum(e.amount for e in l.expenses), + "owner_username": l.owner.username if l.owner else "?" + } + for l in lists + ] + + + return render_template( + "user_expenses.html", + expense_table=expense_table, + lists_data=lists_data + ) @app.route("/user/expenses_data") @@ -2213,6 +2246,10 @@ def handle_add_item(data): name = data["name"].strip() quantity = data.get("quantity", 1) + list_obj = db.session.get(ShoppingList, list_id) + if not list_obj: + return + try: quantity = int(quantity) if quantity < 1: @@ -2248,12 +2285,15 @@ def handle_add_item(data): if max_position is None: max_position = 0 + user_id = current_user.id if current_user.is_authenticated else None + user_name = current_user.username if current_user.is_authenticated else "Gość" + new_item = Item( list_id=list_id, name=name, quantity=quantity, position=max_position + 1, - added_by=current_user.id if current_user.is_authenticated else None, + added_by=user_id, ) db.session.add(new_item) @@ -2271,9 +2311,9 @@ def handle_add_item(data): "id": new_item.id, "name": new_item.name, "quantity": new_item.quantity, - "added_by": ( - current_user.username if current_user.is_authenticated else "Gość" - ), + "added_by": user_name, + "added_by_id": user_id, + "owner_id": list_obj.owner_id, }, to=str(list_id), include_self=True, @@ -2292,6 +2332,7 @@ def handle_add_item(data): ) + @socketio.on("check_item") def handle_check_item(data): # item = Item.query.get(data["item_id"]) @@ -2345,7 +2386,19 @@ def handle_uncheck_item(data): @socketio.on("request_full_list") def handle_request_full_list(data): list_id = data["list_id"] - items = Item.query.filter_by(list_id=list_id).order_by(Item.position.asc()).all() + + shopping_list = db.session.get(ShoppingList, list_id) + if not shopping_list: + return + + owner_id = shopping_list.owner_id + + items = ( + Item.query.options(joinedload(Item.added_by_user)) + .filter_by(list_id=list_id) + .order_by(Item.position.asc()) + .all() + ) items_data = [] for item in items: @@ -2358,12 +2411,16 @@ def handle_request_full_list(data): "not_purchased": item.not_purchased, "not_purchased_reason": item.not_purchased_reason, "note": item.note or "", + "added_by": item.added_by_user.username if item.added_by_user else None, + "added_by_id": item.added_by_user.id if item.added_by_user else None, + "owner_id": owner_id, } ) emit("full_list", {"items": items_data}, to=request.sid) + @socketio.on("update_note") def handle_update_note(data): item_id = data["item_id"] diff --git a/static/js/functions.js b/static/js/functions.js index 6df9da8..664c5ff 100644 --- a/static/js/functions.js +++ b/static/js/functions.js @@ -272,8 +272,98 @@ function isListDifferent(oldItems, newItems) { return false; } -function updateListSmoothly(newItems) { +function renderItem(item, isShare = window.IS_SHARE, showEditOnly = false) { + const li = document.createElement('li'); + li.id = `item-${item.id}`; + li.dataset.name = item.name.toLowerCase(); + li.className = `list-group-item d-flex justify-content-between align-items-center flex-wrap clickable-item ${item.purchased ? 'bg-success text-white' + : item.not_purchased ? 'bg-warning text-dark' + : 'item-not-checked' + }`; + + let quantityBadge = ''; + if (item.quantity && item.quantity > 1) { + quantityBadge = `x${item.quantity}`; + } + + let checkboxOrIcon = item.not_purchased + ? `🚫` + : ``; + + let noteHTML = item.note + ? `[ ${item.note} ]` : ''; + + let reasonHTML = item.not_purchased_reason + ? `[ Powód: ${item.not_purchased_reason} ]` : ''; + + let left = ` +
+ ${window.isSorting ? `` : ''} + ${checkboxOrIcon} + ${item.name} ${quantityBadge} + ${noteHTML} + ${reasonHTML} +
`; + + let rightButtons = ''; + + // ✏️ i 🗑️ — tylko jeśli nie jesteśmy w trybie /share lub jesteśmy w 15s (tymczasowo) + if (!isShare || showEditOnly) { + rightButtons += ` + + `; + } + + // ✅ Jeśli element jest oznaczony jako niekupiony — pokaż "Przywróć" + if (item.not_purchased) { + rightButtons += ` + `; + } + + // ⚠️ tylko jeśli NIE jest oznaczony jako niekupiony i nie jesteśmy w 15s + if (!item.not_purchased && !showEditOnly) { + rightButtons += ` + `; + } + + // 📝 tylko jeśli jesteśmy w /share i nie jesteśmy w 15s + if (isShare && !showEditOnly) { + rightButtons += ` + `; + } + + + li.innerHTML = `${left}
${rightButtons}
`; + + if (item.added_by && item.owner_id && item.added_by_id && item.added_by_id !== item.owner_id) { + const infoEl = document.createElement('small'); + infoEl.className = 'text-info ms-4'; + infoEl.innerHTML = `[Dodane przez: ${item.added_by}]`; + li.querySelector('.d-flex.align-items-center')?.appendChild(infoEl); + } + + return li; +} + + + +function updateListSmoothly(newItems) { const itemsContainer = document.getElementById('items'); const existingItemsMap = new Map(); @@ -285,68 +375,7 @@ function updateListSmoothly(newItems) { const fragment = document.createDocumentFragment(); newItems.forEach(item => { - let li = existingItemsMap.get(item.id); - let quantityBadge = ''; - if (item.quantity && item.quantity > 1) { - quantityBadge = `x${item.quantity}`; - } - - if (!li) { - li = document.createElement('li'); - li.id = `item-${item.id}`; - } - - // Klasy tła - li.className = `list-group-item d-flex justify-content-between align-items-center flex-wrap clickable-item ${item.purchased ? 'bg-success text-white' : - item.not_purchased ? 'bg-warning text-dark' : 'item-not-checked' - }`; - - // Wewnętrzny HTML - li.innerHTML = ` -
- ${isSorting ? `` : ''} - ${!item.not_purchased ? ` - - ` : ` - 🚫 - `} - ${item.name} ${quantityBadge} - - ${item.note ? `[ ${item.note} ]` : ''} - ${item.not_purchased_reason ? `[ Powód: ${item.not_purchased_reason} ]` : ''} -
-
- ${item.not_purchased ? ` - - ` : ` - - ${window.IS_SHARE ? ` - - ` : ''} - `} - ${!window.IS_SHARE ? ` - - - ` : ''} -
- `; - + const li = renderItem(item); fragment.appendChild(li); }); diff --git a/static/js/live.js b/static/js/live.js index e96ebdb..4f13c51 100644 --- a/static/js/live.js +++ b/static/js/live.js @@ -127,69 +127,59 @@ function setupList(listId, username) { showToast(`Dodano wydatek: ${data.amount.toFixed(2)} PLN`, 'info'); }); + socket.on('item_added', data => { showToast(`${data.added_by} dodał: ${data.name}`, 'info'); - const li = document.createElement('li'); - li.className = 'list-group-item d-flex justify-content-between align-items-center flex-wrap item-not-checked'; - li.id = `item-${data.id}`; - let quantityBadge = ''; - if (data.quantity && data.quantity > 1) { - quantityBadge = `x${data.quantity}`; - } - - const countdownId = `countdown-${data.id}`; - const countdownBtn = ` - - `; - - li.innerHTML = ` -
- - - ${data.name} ${quantityBadge} - -
-
- ${countdownBtn} - - -
- `; + const item = { + ...data, + purchased: false, + not_purchased: false, + not_purchased_reason: '', + note: '' + }; + const li = renderItem(item, false, true); // ← tryb 15s document.getElementById('items').appendChild(li); toggleEmptyPlaceholder(); + updateProgressBar(); - // ⏳ 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); + if (window.IS_SHARE) { + const countdownId = `countdown-${data.id}`; + const countdownBtn = document.createElement('button'); + countdownBtn.type = 'button'; + countdownBtn.className = 'btn btn-outline-warning'; + countdownBtn.id = countdownId; + countdownBtn.disabled = true; + countdownBtn.textContent = '15s'; - // 🔁 Request listy po 15s - setTimeout(() => { - if (window.LIST_ID) { - socket.emit('request_full_list', { list_id: window.LIST_ID }); - } - }, 15000); + li.querySelector('.btn-group')?.prepend(countdownBtn); + + let seconds = 15; + const intervalId = setInterval(() => { + const el = document.getElementById(countdownId); + if (el) { + seconds--; + el.textContent = `${seconds}s`; + if (seconds <= 0) { + el.remove(); + clearInterval(intervalId); + } + } else { + clearInterval(intervalId); + } + }, 1000); + + setTimeout(() => { + const existing = document.getElementById(`item-${data.id}`); + if (existing) { + const updated = renderItem(item, true); + existing.replaceWith(updated); + } + }, 15000); + } }); - - socket.on('item_deleted', data => { const li = document.getElementById(`item-${data.item_id}`); if (li) { diff --git a/static/js/user_expense_lists.js b/static/js/user_expense_lists.js new file mode 100644 index 0000000..fa6cdd9 --- /dev/null +++ b/static/js/user_expense_lists.js @@ -0,0 +1,103 @@ +document.addEventListener('DOMContentLoaded', () => { + const checkboxes = document.querySelectorAll('.list-checkbox'); + const totalEl = document.getElementById('listsTotal'); + const filterButtons = document.querySelectorAll('.filter-btn'); + const rows = document.querySelectorAll('#listsTableBody tr'); + + const onlyWith = document.getElementById('onlyWithExpenses'); + const customStart = document.getElementById('customStart'); + const customEnd = document.getElementById('customEnd'); + + function updateTotal() { + let total = 0; + checkboxes.forEach(cb => { + const row = cb.closest('tr'); + if (cb.checked && row.style.display !== 'none') { + total += parseFloat(cb.dataset.amount); + } + }); + + totalEl.textContent = total.toFixed(2) + ' PLN'; + totalEl.parentElement.classList.add('animate__animated', 'animate__fadeIn'); + setTimeout(() => { + totalEl.parentElement.classList.remove('animate__animated', 'animate__fadeIn'); + }, 400); + } + + checkboxes.forEach(cb => cb.addEventListener('change', updateTotal)); + + filterButtons.forEach(btn => { + btn.addEventListener('click', () => { + filterButtons.forEach(b => b.classList.remove('active')); + btn.classList.add('active'); + const range = btn.dataset.range; + + const now = new Date(); + const todayStr = now.toISOString().slice(0, 10); + const year = now.getFullYear(); + const month = now.toISOString().slice(0, 7); + const week = `${year}-${String(getISOWeek(now)).padStart(2, '0')}`; + + rows.forEach(row => { + const rDate = row.dataset.date; + const rMonth = row.dataset.month; + const rWeek = row.dataset.week; + const rYear = row.dataset.year; + + let show = true; + if (range === 'day') show = rDate === todayStr; + if (range === 'month') show = rMonth === month; + if (range === 'week') show = rWeek === week; + if (range === 'year') show = rYear === String(year); + + row.style.display = show ? '' : 'none'; + }); + + applyExpenseFilter(); + updateTotal(); + }); + }); + + // ISO week helper + function getISOWeek(date) { + const target = new Date(date.valueOf()); + const dayNr = (date.getDay() + 6) % 7; + target.setDate(target.getDate() - dayNr + 3); + const firstThursday = new Date(target.getFullYear(), 0, 4); + const dayDiff = (target - firstThursday) / 86400000; + return 1 + Math.floor(dayDiff / 7); + } + + // Zakres dat: kliknij „Zastosuj zakres” + document.getElementById('applyCustomRange').addEventListener('click', () => { + const start = customStart.value; + const end = customEnd.value; + + filterButtons.forEach(b => b.classList.remove('active')); + + rows.forEach(row => { + const date = row.dataset.date; + const show = (!start || date >= start) && (!end || date <= end); + row.style.display = show ? '' : 'none'; + }); + + applyExpenseFilter(); + updateTotal(); + }); + + // Filtrowanie tylko list z wydatkami + if (onlyWith) { + onlyWith.addEventListener('change', () => { + applyExpenseFilter(); + updateTotal(); + }); + } + + function applyExpenseFilter() { + if (!onlyWith || !onlyWith.checked) return; + rows.forEach(row => { + const amt = parseFloat(row.querySelector('.list-checkbox').dataset.amount || 0); + if (amt <= 0) row.style.display = 'none'; + }); + } +}); diff --git a/templates/list.html b/templates/list.html index 7a69337..c1830d1 100644 --- a/templates/list.html +++ b/templates/list.html @@ -131,11 +131,11 @@ {% endif %} {% if not is_share %} - - diff --git a/templates/user_expenses.html b/templates/user_expenses.html index d351519..2623592 100644 --- a/templates/user_expenses.html +++ b/templates/user_expenses.html @@ -10,12 +10,14 @@
- -
+ + +
- {% if expense_table %} -
- {% for row in expense_table %} -
-
-
-
{{ row.title }}
-

💸 {{ '%.2f'|format(row.amount) }} PLN

-

📅 {{ row.added_at.strftime('%Y-%m-%d') }}

-
-
-
- {% endfor %} + +
+ + + + +
- {% else %} -
Brak wydatków do wyświetlenia.
- {% endif %} + +
+
+ +
+
+ +
+
+ +
+
+
+ + +
+
+ + + + + + + + + + + {% for list in lists_data %} + + + + + + + {% endfor %} + +
Nazwa listyDataWydatki (PLN)
+ + + {{ list.title }} +
👤 {{ list.owner_username or '?' }} + +
{{ list.created_at.strftime('%Y-%m-%d') }}{{ '%.2f'|format(list.total_expense) }}
+
+ +
+
💰 Suma zaznaczonych: 0.00 PLN
- + +
@@ -78,6 +125,7 @@
+
@@ -86,4 +134,5 @@ {% block scripts %} + {% endblock %} \ No newline at end of file From 0d5b170caccfae454ae87c16a760568792c783ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Fri, 25 Jul 2025 10:42:07 +0200 Subject: [PATCH 02/47] zmiany w sablonach i poprawki w ocr --- app.py | 179 ++++++++++++++++---------------- static/js/functions.js | 2 +- static/js/user_expense_lists.js | 25 +++++ templates/list.html | 30 +++--- templates/list_share.html | 16 +-- templates/user_expenses.html | 69 ++++++------ 6 files changed, 173 insertions(+), 148 deletions(-) diff --git a/app.py b/app.py index a670afc..6b494e2 100644 --- a/app.py +++ b/app.py @@ -115,7 +115,7 @@ class ShoppingList(db.Model): id = db.Column(db.Integer, primary_key=True) title = db.Column(db.String(150), nullable=False) created_at = db.Column(db.DateTime, default=datetime.utcnow) - + owner_id = db.Column(db.Integer, db.ForeignKey("user.id")) owner = db.relationship("User", backref="lists", foreign_keys=[owner_id]) @@ -135,7 +135,9 @@ class Item(db.Model): # added_at = db.Column(db.DateTime, default=datetime.utcnow) added_at = db.Column(db.DateTime, default=utcnow) added_by = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=True) - added_by_user = db.relationship("User", backref="added_items", lazy=True, foreign_keys=[added_by]) + added_by_user = db.relationship( + "User", backref="added_items", lazy=True, foreign_keys=[added_by] + ) purchased = db.Column(db.Boolean, default=False) purchased_at = db.Column(db.DateTime, nullable=True) @@ -393,22 +395,25 @@ def preprocess_image_for_tesseract(image): def extract_total_tesseract(image): + import pytesseract + from pytesseract import Output + import re + text = pytesseract.image_to_string(image, lang="pol", config="--psm 4") lines = text.splitlines() candidates = [] - keyword_lines_debug = [] - fuzzy_regex = re.compile(r"[\dOo][.,:;g9zZ][\d]{2}") - keyword_pattern = re.compile( + blacklist_keywords = re.compile(r"\b(ptu|vat|podatek|stawka)\b", re.IGNORECASE) + + priority_keywords = re.compile( r""" \b( - [5s]u[mn][aąo0]? | - razem | - zap[łl][aąo0]ty | - do\s+zap[łl][aąo0]ty | + razem\s*do\s*zap[łl][aąo0]ty | + do\s*zap[łl][aąo0]ty | + suma | kwota | - płatno[śćs] | warto[śćs] | + płatno[śćs] | total | amount )\b @@ -416,84 +421,71 @@ def extract_total_tesseract(image): re.IGNORECASE | re.VERBOSE, ) - for idx, line in enumerate(lines): - if keyword_pattern.search(line[:30]): - keyword_lines_debug.append((idx, line)) - for line in lines: if not line.strip(): continue - matches = re.findall(r"\d{1,4}\s?[.,]\d{2}", line) + if blacklist_keywords.search(line): + continue + + is_priority = priority_keywords.search(line) + + matches = re.findall(r"\d{1,4}[.,]\d{2}", line) for match in matches: try: - val = float(match.replace(" ", "").replace(",", ".")) + val = float(match.replace(",", ".")) if 0.1 <= val <= 100000: - candidates.append((val, line)) + candidates.append((val, line, is_priority is not None)) except: continue - spaced = re.findall(r"\d{1,4}\s\d{2}", line) - for match in spaced: - try: - val = float(match.replace(" ", ".")) - if 0.1 <= val <= 100000: - candidates.append((val, line)) - except: - continue + # Tylko w liniach priorytetowych: sprawdzamy spaced fallback + if is_priority: + spaced = re.findall(r"\d{1,4}\s\d{2}", line) + for match in spaced: + try: + val = float(match.replace(" ", ".")) + if 0.1 <= val <= 100000: + candidates.append((val, line, True)) + except: + continue - fuzzy_matches = fuzzy_regex.findall(line) - for match in fuzzy_matches: - cleaned = ( - match.replace("O", "0") - .replace("o", "0") - .replace(":", ".") - .replace(";", ".") - .replace(",", ".") - .replace("g", "9") - .replace("z", "9") - .replace("Z", "9") - ) - try: - val = float(cleaned) - if 0.1 <= val <= 100000: - candidates.append((val, line)) - except: - continue - - preferred = [ - (val, line) for val, line in candidates if keyword_pattern.search(line.lower()) - ] + # Preferujemy linie priorytetowe + preferred = [(val, line) for val, line, is_pref in candidates if is_pref] if preferred: - max_val = max(preferred, key=lambda x: x[0])[0] - return round(max_val, 2), lines + best_val = max(preferred, key=lambda x: x[0])[0] + if best_val < 99999: + return round(best_val, 2), lines if candidates: - max_val = max([val for val, _ in candidates]) - return round(max_val, 2), lines + best_val = max(candidates, key=lambda x: x[0])[0] + if best_val < 99999: + return round(best_val, 2), lines + # Fallback: największy font + bold data = pytesseract.image_to_data( image, lang="pol", config="--psm 4", output_type=Output.DICT ) - font_candidates = [] + font_candidates = [] for i in range(len(data["text"])): word = data["text"][i].strip() - if not word: + if not word or not re.match(r"^\d{1,5}[.,\s]\d{2}$", word): continue - if re.match(r"^\d{1,5}[.,\s]\d{2}$", word): - try: - val = float(word.replace(",", ".").replace(" ", ".")) - height = data["height"][i] - if 0.1 <= val <= 10000: - font_candidates.append((val, height, word)) - except: - continue + try: + val = float(word.replace(",", ".").replace(" ", ".")) + height = data["height"][i] + conf = int(data.get("conf", ["0"] * len(data["text"]))[i]) + if 0.1 <= val <= 100000: + font_candidates.append((val, height, conf)) + except: + continue if font_candidates: - best = max(font_candidates, key=lambda x: x[1]) + # Preferuj najwyższy font z sensownym confidence + best = max(font_candidates, key=lambda x: (x[1], x[2])) return round(best[0], 2), lines return 0.0, lines @@ -964,15 +956,32 @@ def view_list(list_id): @app.route("/user_expenses") @login_required def user_expenses(): - # Lista wydatków użytkownika - expenses = ( + start_date_str = request.args.get("start_date") + end_date_str = request.args.get("end_date") + start = None + end = None + + # Przygotowanie podstawowego zapytania o wydatki użytkownika + expenses_query = ( Expense.query.join(ShoppingList, Expense.list_id == ShoppingList.id) .options(joinedload(Expense.list)) .filter(ShoppingList.owner_id == current_user.id) - .order_by(Expense.added_at.desc()) - .all() ) + # Filtrowanie po zakresie dat, jeśli podano + if start_date_str and end_date_str: + try: + start = datetime.strptime(start_date_str, "%Y-%m-%d") + end = datetime.strptime(end_date_str, "%Y-%m-%d") + timedelta(days=1) + expenses_query = expenses_query.filter( + Expense.added_at >= start, Expense.added_at < end + ) + except ValueError: + flash("Błędny zakres dat", "danger") + + expenses = expenses_query.order_by(Expense.added_at.desc()).all() + + # Tabela wydatków expense_table = [ { "title": e.list.title if e.list else "Nieznana", @@ -982,34 +991,32 @@ def user_expenses(): for e in expenses ] + # Tylko listy z tych wydatków + list_ids = {e.list_id for e in expenses} lists = ( - ShoppingList.query - .filter( - or_( - ShoppingList.owner_id == current_user.id, - ShoppingList.is_public == True - ) - ) + ShoppingList.query.filter(ShoppingList.id.in_(list_ids)) .order_by(ShoppingList.created_at.desc()) .all() ) + # Lista zsumowanych wydatków per lista (z uwzględnieniem filtra dat) lists_data = [ { "id": l.id, "title": l.title, "created_at": l.created_at, - "total_expense": sum(e.amount for e in l.expenses), - "owner_username": l.owner.username if l.owner else "?" + "total_expense": sum( + e.amount + for e in l.expenses + if (not start or not end) or (e.added_at >= start and e.added_at < end) + ), + "owner_username": l.owner.username if l.owner else "?", } for l in lists ] - return render_template( - "user_expenses.html", - expense_table=expense_table, - lists_data=lists_data + "user_expenses.html", expense_table=expense_table, lists_data=lists_data ) @@ -1028,7 +1035,7 @@ def user_expenses_data(): try: start = datetime.strptime(start_date, "%Y-%m-%d") end = datetime.strptime(end_date, "%Y-%m-%d") + timedelta(days=1) - query = query.filter(Expense.timestamp >= start, Expense.timestamp < end) + query = query.filter(Expense.added_at >= start, Expense.added_at < end) except ValueError: return jsonify({"error": "Błędne daty"}), 400 @@ -2332,7 +2339,6 @@ def handle_add_item(data): ) - @socketio.on("check_item") def handle_check_item(data): # item = Item.query.get(data["item_id"]) @@ -2420,7 +2426,6 @@ def handle_request_full_list(data): emit("full_list", {"items": items_data}, to=request.sid) - @socketio.on("update_note") def handle_update_note(data): item_id = data["item_id"] @@ -2490,16 +2495,6 @@ def handle_unmark_not_purchased(data): emit("item_unmarked_not_purchased", {"item_id": item.id}, to=str(item.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/js/functions.js b/static/js/functions.js index 664c5ff..fcabe77 100644 --- a/static/js/functions.js +++ b/static/js/functions.js @@ -354,7 +354,7 @@ function renderItem(item, isShare = window.IS_SHARE, showEditOnly = false) { if (item.added_by && item.owner_id && item.added_by_id && item.added_by_id !== item.owner_id) { const infoEl = document.createElement('small'); infoEl.className = 'text-info ms-4'; - infoEl.innerHTML = `[Dodane przez: ${item.added_by}]`; + infoEl.innerHTML = `[Dodał/a: ${item.added_by}]`; li.querySelector('.d-flex.align-items-center')?.appendChild(infoEl); } diff --git a/static/js/user_expense_lists.js b/static/js/user_expense_lists.js index fa6cdd9..2ceb887 100644 --- a/static/js/user_expense_lists.js +++ b/static/js/user_expense_lists.js @@ -101,3 +101,28 @@ document.addEventListener('DOMContentLoaded', () => { }); } }); + +document.addEventListener("DOMContentLoaded", function () { + const toggleBtn = document.getElementById("toggleAllCheckboxes"); + let allChecked = false; + + toggleBtn?.addEventListener("click", () => { + const checkboxes = document.querySelectorAll(".list-checkbox"); + allChecked = !allChecked; + + checkboxes.forEach(cb => { + cb.checked = allChecked; + }); + + toggleBtn.textContent = allChecked ? "🚫 Odznacz wszystkie" : "✅ Zaznacz wszystkie"; + }); +}); + +document.getElementById("applyCustomRange")?.addEventListener("click", () => { + const start = document.getElementById("customStart")?.value; + const end = document.getElementById("customEnd")?.value; + if (start && end) { + const url = `/user_expenses?start_date=${start}&end_date=${end}`; + window.location.href = url; + } +}); \ No newline at end of file diff --git a/templates/list.html b/templates/list.html index c1830d1..000e4dc 100644 --- a/templates/list.html +++ b/templates/list.html @@ -118,26 +118,26 @@
- {% if item.not_purchased %} - - {% else %} - {% endif %} - {% if not is_share %} - - {% endif %}
diff --git a/templates/list_share.html b/templates/list_share.html index 495065f..6e9cf36 100644 --- a/templates/list_share.html +++ b/templates/list_share.html @@ -53,21 +53,23 @@
{% if item.not_purchased %} - {% else %} - - - {% endif %}
+ {% else %}
  • diff --git a/templates/user_expenses.html b/templates/user_expenses.html index 2623592..7c3fd3d 100644 --- a/templates/user_expenses.html +++ b/templates/user_expenses.html @@ -34,29 +34,32 @@
    - - - - - + + + + +
    -
    -
    - -
    -
    - -
    -
    - -
    -
    -
    + +
    - + +
    + +
    + Od + + Do + + +
    + +
    +
    @@ -106,29 +109,29 @@ -
    + +
    - +
    -
    -
    - -
    -
    - -
    -
    - -
    + +
    + Od + + Do + +
    -
    +
    + + {% endblock %} {% block scripts %} From bb667a2cbdc3ccfc878f285bc1720b204657449b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Fri, 25 Jul 2025 10:53:50 +0200 Subject: [PATCH 03/47] poprawki w user_expenses --- app.py | 44 +++++++++++++++++++-------------- static/js/user_expense_lists.js | 13 ++++++++++ templates/user_expenses.html | 5 ++++ 3 files changed, 44 insertions(+), 18 deletions(-) diff --git a/app.py b/app.py index 6b494e2..6014b47 100644 --- a/app.py +++ b/app.py @@ -958,17 +958,25 @@ def view_list(list_id): def user_expenses(): start_date_str = request.args.get("start_date") end_date_str = request.args.get("end_date") + show_all = request.args.get("show_all", "false").lower() == "true" + start = None end = None - # Przygotowanie podstawowego zapytania o wydatki użytkownika - expenses_query = ( - Expense.query.join(ShoppingList, Expense.list_id == ShoppingList.id) - .options(joinedload(Expense.list)) - .filter(ShoppingList.owner_id == current_user.id) - ) + expenses_query = Expense.query.join( + ShoppingList, Expense.list_id == ShoppingList.id + ).options(joinedload(Expense.list)) + + # Jeśli show_all to False, filtruj tylko po bieżącym użytkowniku + if not show_all: + expenses_query = expenses_query.filter(ShoppingList.owner_id == current_user.id) + else: + expenses_query = expenses_query.filter( + or_( + ShoppingList.owner_id == current_user.id, ShoppingList.is_public == True + ) + ) - # Filtrowanie po zakresie dat, jeśli podano if start_date_str and end_date_str: try: start = datetime.strptime(start_date_str, "%Y-%m-%d") @@ -981,7 +989,13 @@ def user_expenses(): expenses = expenses_query.order_by(Expense.added_at.desc()).all() - # Tabela wydatków + list_ids = {e.list_id for e in expenses} + lists = ( + ShoppingList.query.filter(ShoppingList.id.in_(list_ids)) + .order_by(ShoppingList.created_at.desc()) + .all() + ) + expense_table = [ { "title": e.list.title if e.list else "Nieznana", @@ -991,15 +1005,6 @@ def user_expenses(): for e in expenses ] - # Tylko listy z tych wydatków - list_ids = {e.list_id for e in expenses} - lists = ( - ShoppingList.query.filter(ShoppingList.id.in_(list_ids)) - .order_by(ShoppingList.created_at.desc()) - .all() - ) - - # Lista zsumowanych wydatków per lista (z uwzględnieniem filtra dat) lists_data = [ { "id": l.id, @@ -1016,7 +1021,10 @@ def user_expenses(): ] return render_template( - "user_expenses.html", expense_table=expense_table, lists_data=lists_data + "user_expenses.html", + expense_table=expense_table, + lists_data=lists_data, + show_all=show_all, ) diff --git a/static/js/user_expense_lists.js b/static/js/user_expense_lists.js index 2ceb887..7c7460f 100644 --- a/static/js/user_expense_lists.js +++ b/static/js/user_expense_lists.js @@ -115,6 +115,8 @@ document.addEventListener("DOMContentLoaded", function () { }); toggleBtn.textContent = allChecked ? "🚫 Odznacz wszystkie" : "✅ Zaznacz wszystkie"; + const updateTotalEvent = new Event('change'); + checkboxes.forEach(cb => cb.dispatchEvent(updateTotalEvent)); }); }); @@ -125,4 +127,15 @@ document.getElementById("applyCustomRange")?.addEventListener("click", () => { const url = `/user_expenses?start_date=${start}&end_date=${end}`; window.location.href = url; } +}); + +document.getElementById("showAllLists").addEventListener("change", function () { + const checked = this.checked; + const url = new URL(window.location.href); + if (checked) { + url.searchParams.set("show_all", "true"); + } else { + url.searchParams.delete("show_all"); + } + window.location.href = url.toString(); }); \ No newline at end of file diff --git a/templates/user_expenses.html b/templates/user_expenses.html index 7c3fd3d..17f5852 100644 --- a/templates/user_expenses.html +++ b/templates/user_expenses.html @@ -47,6 +47,11 @@ +
    + + +
    Od 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 04/47] 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 From cd06fc3ca4e12e1f6a0dff50026485fc4189b7a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Fri, 25 Jul 2025 18:29:32 +0200 Subject: [PATCH 05/47] nowe funkcje i fixy --- static/css/style.css | 8 -------- 1 file changed, 8 deletions(-) diff --git a/static/css/style.css b/static/css/style.css index d7e1dea..0174ec6 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -309,12 +309,4 @@ 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 From be986fc8f5164c7a92f55308a8f8ef993ccea93c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Fri, 25 Jul 2025 18:33:16 +0200 Subject: [PATCH 06/47] poprawki w compose --- .env.example | 8 ++++---- docker-compose.yml | 8 +------- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/.env.example b/.env.example index b6961b1..637f519 100644 --- a/.env.example +++ b/.env.example @@ -37,17 +37,17 @@ DB_ENGINE=sqlite # Ustaw DB_ENGINE=pgsql # Domyslny port PostgreSQL to 5432 # Wymaga dzialajacego serwera PostgreSQL (np. kontener `postgres`) -# Przyklad URI: postgresql://user:pass@db:5432/myapp # --- Konfiguracja dla mysql --- # Ustaw DB_ENGINE=mysql # Domyslny port MySQL to 3306 # Wymaga kontenera z MySQL i uzytkownika z dostepem do bazy -# Przyklad URI: mysql+pymysql://user:pass@db:3306/myapp # Wspolne zmienne (dla pgsql, mysql) -DB_HOST=db +# DB_HOST = pgsql lub mysql zgodnie z deployem (profil w docker-compose.yml) + +DB_HOST=pgsql DB_PORT=5432 DB_NAME=myapp DB_USER=user -DB_PASSWORD=pass +DB_PASSWORD=pass \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 19ec0ed..cfc632d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -27,10 +27,7 @@ services: POSTGRES_PASSWORD: ${DB_PASSWORD} volumes: - ./db/pgsql:/var/lib/postgresql/data - #ports: - # - ":5432:5432" restart: unless-stopped - hostname: db profiles: ["pgsql"] mysql: @@ -43,8 +40,5 @@ services: MYSQL_ROOT_PASSWORD: 89o38kUX5T4C volumes: - ./db/mysql:/var/lib/mysql - #ports: - # - "3306:3306" restart: unless-stopped - hostname: db - profiles: ["mysql"] + profiles: ["mysql"] \ No newline at end of file From 5e782ba170e1692aeedc23b05f8e1f34d40947dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Fri, 25 Jul 2025 19:01:52 +0200 Subject: [PATCH 07/47] flask-talisman + naglowki --- .env.example | 27 ++++++++++++++++++++++++++- app.py | 23 +++++++++++++++++++++++ config.py | 5 +++++ requirements.txt | 1 + 4 files changed, 55 insertions(+), 1 deletion(-) diff --git a/.env.example b/.env.example index 637f519..2d55082 100644 --- a/.env.example +++ b/.env.example @@ -50,4 +50,29 @@ DB_HOST=pgsql DB_PORT=5432 DB_NAME=myapp DB_USER=user -DB_PASSWORD=pass \ No newline at end of file +DB_PASSWORD=pass + + +# ======================== +# Nagłówki bezpieczeństwa +# ======================== + +# ENABLE_HSTS: +# Wymusza HTTPS poprzez ustawienie nagłówka Strict-Transport-Security. +# Zalecane (1) jeśli aplikacja działa za HTTPS. Ustaw 0, jeśli korzystasz z HTTP lokalnie. +ENABLE_HSTS=1 + +# ENABLE_XFO: +# Ustawia nagłówek X-Frame-Options: DENY, który blokuje osadzanie strony w