diff --git a/.env.example b/.env.example index 8954f77..28a66f8 100644 --- a/.env.example +++ b/.env.example @@ -134,29 +134,26 @@ DISABLE_ROBOTS=0 # ======================== # Nagłówki cache # ======================== -# -# Stosowany jest dlugi. Cache-Control z "must-revalidate" -# bo przegladarka dostaje ETag aby mogla walidowac waznosc pliku # JS_CACHE_CONTROL: # Nagłówki Cache-Control dla plików JS (/static/js/) -# Domyślnie: "no-cache, max-age=86400" -JS_CACHE_CONTROL="no-cache, max-age=86400" +# Domyślnie: "no-cache" +JS_CACHE_CONTROL="no-cache" # CSS_CACHE_CONTROL: # Nagłówki Cache-Control dla plików CSS (/static/css/) -# Domyślnie: "no-cache, max-age=86400" -CSS_CACHE_CONTROL="no-cache, max-age=86400" +# Domyślnie: "no-cache" +CSS_CACHE_CONTROL="no-cache" # LIB_JS_CACHE_CONTROL: # Nagłówki Cache-Control dla bibliotek JS (/static/lib/js/) -# Domyślnie: "no-cache, max-age=2592000" -LIB_JS_CACHE_CONTROL="no-cache, max-age=2592000" +# Domyślnie: "max-age=86400" +LIB_JS_CACHE_CONTROL="max-age=86400" # LIB_CSS_CACHE_CONTROL: # Nagłówki Cache-Control dla bibliotek CSS (/static/lib/css/) -# Domyślnie: "must-revalidate, max-age=2592000" -LIB_CSS_CACHE_CONTROL="no-cache, max-age=2592000" +# Domyślnie: "max-age=86400" +LIB_CSS_CACHE_CONTROL="max-age=86400" # UPLOADS_CACHE_CONTROL: # Nagłówki Cache-Control dla wgrywanych plików (/uploads/) diff --git a/app.py b/app.py index 711036e..435b822 100644 --- a/app.py +++ b/app.py @@ -50,7 +50,7 @@ from collections import defaultdict, deque from functools import wraps from flask_talisman import Talisman from flask_session import Session - +from types import SimpleNamespace # OCR import pytesseract @@ -345,7 +345,7 @@ with app.app_context(): db.session.add_all(Category(name=cat) for cat in missing) db.session.commit() print(f"[INFO] Dodano brakujące kategorie: {', '.join(missing)}") - #else: + # else: # print("[INFO] Wszystkie domyślne kategorie już istnieją") @@ -357,7 +357,7 @@ def serve_js(filename): # response.cache_control.must_revalidate = True response.headers["Cache-Control"] = app.config["JS_CACHE_CONTROL"] response.headers.pop("Content-Disposition", None) - #response.headers.pop("Etag", None) + # response.headers.pop("Etag", None) return response @@ -366,7 +366,7 @@ def serve_css(filename): response = send_from_directory("static/css", filename) response.headers["Cache-Control"] = app.config["CSS_CACHE_CONTROL"] response.headers.pop("Content-Disposition", None) - #response.headers.pop("Etag", None) + # response.headers.pop("Etag", None) return response @@ -375,7 +375,7 @@ def serve_js_lib(filename): response = send_from_directory("static/lib/js", filename) response.headers["Cache-Control"] = app.config["LIB_JS_CACHE_CONTROL"] response.headers.pop("Content-Disposition", None) - #response.headers.pop("Etag", None) + # response.headers.pop("Etag", None) return response @@ -384,7 +384,7 @@ def serve_css_lib(filename): response = send_from_directory("static/lib/css", filename) response.headers["Cache-Control"] = app.config["LIB_CSS_CACHE_CONTROL"] response.headers.pop("Content-Disposition", None) - #response.headers.pop("Etag", None) + # response.headers.pop("Etag", None) return response @@ -637,17 +637,45 @@ def get_total_expenses_grouped_by_list_created_at( lists_query = lists_query.filter(ShoppingList.owner_id == user_id) if category_id: - lists_query = lists_query.join( - shopping_list_category, - shopping_list_category.c.shopping_list_id == ShoppingList.id, - ).filter(shopping_list_category.c.category_id == category_id) + if str(category_id) == "none": # Bez kategorii + lists_query = lists_query.filter(~ShoppingList.categories.any()) + else: + try: + cat_id_int = int(category_id) + except ValueError: + return {"labels": [], "expenses": []} + lists_query = lists_query.join( + shopping_list_category, + shopping_list_category.c.shopping_list_id == ShoppingList.id, + ).filter(shopping_list_category.c.category_id == cat_id_int) + + # Obsługa nowych zakresów + today = datetime.now(timezone.utc).date() + + if range_type == "last30days": + dt_start = today - timedelta(days=29) + dt_end = today + timedelta(days=1) + start_date, end_date = dt_start.strftime("%Y-%m-%d"), dt_end.strftime( + "%Y-%m-%d" + ) + + elif range_type == "currentmonth": + dt_start = today.replace(day=1) + dt_end = today + timedelta(days=1) + start_date, end_date = dt_start.strftime("%Y-%m-%d"), dt_end.strftime( + "%Y-%m-%d" + ) if start_date and end_date: try: dt_start = datetime.strptime(start_date, "%Y-%m-%d") - dt_end = datetime.strptime(end_date, "%Y-%m-%d") + timedelta(days=1) + dt_end = datetime.strptime(end_date, "%Y-%m-%d") + if dt_end.tzinfo is None: + dt_end = dt_end.replace(tzinfo=timezone.utc) + dt_end += timedelta(days=1) except Exception: return {"error": "Błędne daty", "labels": [], "expenses": []} + lists_query = lists_query.filter( ShoppingList.created_at >= dt_start, ShoppingList.created_at < dt_end ) @@ -658,7 +686,6 @@ def get_total_expenses_grouped_by_list_created_at( list_ids = [l.id for l in lists] - # Suma wszystkich wydatków dla każdej listy total_expenses = ( db.session.query( Expense.list_id, func.sum(Expense.amount).label("total_amount") @@ -674,7 +701,9 @@ def get_total_expenses_grouped_by_list_created_at( for sl in lists: if sl.id in expense_map: ts = sl.created_at or datetime.now(timezone.utc) - if range_type == "monthly": + if range_type in ("last30days", "currentmonth"): + key = ts.strftime("%Y-%m-%d") # dzienny widok + elif range_type == "monthly": key = ts.strftime("%Y-%m") elif range_type == "quarterly": key = f"{ts.year}-Q{((ts.month - 1) // 3 + 1)}" @@ -794,18 +823,19 @@ def get_admin_expense_summary(): return f"#{r:02x}{g:02x}{b:02x}" """ + def category_to_color(name): hash_val = int(hashlib.md5(name.encode("utf-8")).hexdigest(), 16) hue = (hash_val % 360) / 360.0 saturation = 0.60 + ((hash_val >> 8) % 17) / 100.0 - lightness = 0.28 + ((hash_val >> 16) % 11) / 100.0 + lightness = 0.28 + ((hash_val >> 16) % 11) / 100.0 r, g, b = colorsys.hls_to_rgb(hue, lightness, saturation) return f"#{int(r*255):02x}{int(g*255):02x}{int(b*255):02x}" def get_total_expenses_grouped_by_category( - show_all, range_type, start_date, end_date, user_id + show_all, range_type, start_date, end_date, user_id, category_id=None ): lists_query = ShoppingList.query @@ -816,6 +846,19 @@ def get_total_expenses_grouped_by_category( else: lists_query = lists_query.filter(ShoppingList.owner_id == user_id) + if category_id: + if str(category_id) == "none": # Bez kategorii + lists_query = lists_query.filter(~ShoppingList.categories.any()) + else: + try: + cat_id_int = int(category_id) + except ValueError: + return {"labels": [], "datasets": []} + lists_query = lists_query.join( + shopping_list_category, + shopping_list_category.c.shopping_list_id == ShoppingList.id, + ).filter(shopping_list_category.c.category_id == cat_id_int) + if start_date and end_date: try: dt_start = datetime.strptime(start_date, "%Y-%m-%d") @@ -856,10 +899,19 @@ def get_total_expenses_grouped_by_category( all_labels.add(key) + # Specjalna obsługa dla filtra "Bez kategorii" + if str(category_id) == "none": + if not l.categories: + data_map[key]["Bez kategorii"] += total_expense + continue # 🔹 Pomijamy dalsze dodawanie innych kategorii + + # Standardowa logika if not l.categories: - data_map[key]["Inne"] += total_expense + data_map[key]["Bez kategorii"] += total_expense else: for c in l.categories: + if category_id and str(c.id) != str(category_id): + continue data_map[key][c.name] += total_expense labels = sorted(all_labels) @@ -1121,9 +1173,13 @@ def log_request(response): duration = round((time.time() - start) * 1000, 2) if start else "-" agent = request.headers.get("User-Agent", "-") if status == 304: - app.logger.info(f"REVALIDATED: {ip} - \"{method} {path}\" {status} {length} {duration}ms \"{agent}\"") + app.logger.info( + f'REVALIDATED: {ip} - "{method} {path}" {status} {length} {duration}ms "{agent}"' + ) else: - app.logger.info(f'{ip} - "{method} {path}" {status} {length} {duration}ms "{agent}"') + app.logger.info( + f'{ip} - "{method} {path}" {status} {length} {duration}ms "{agent}"' + ) app.logger.debug(f"Request headers: {dict(request.headers)}") app.logger.debug(f"Response headers: {dict(response.headers)}") return response @@ -1600,9 +1656,9 @@ def view_list(list_id): ) -@app.route("/user_expenses") +@app.route("/expenses") @login_required -def user_expenses(): +def expenses(): start_date_str = request.args.get("start_date") end_date_str = request.args.get("end_date") category_id = request.args.get("category_id", type=int) @@ -1631,6 +1687,8 @@ def user_expenses(): .all() ) + categories.append(SimpleNamespace(id="none", name="Bez kategorii")) + start = None end = None @@ -1650,10 +1708,13 @@ def user_expenses(): ) if category_id: - expenses_query = expenses_query.join( - shopping_list_category, - shopping_list_category.c.shopping_list_id == ShoppingList.id, - ).filter(shopping_list_category.c.category_id == category_id) + if str(category_id) == "none": # Bez kategorii + lists_query = lists_query.filter(~ShoppingList.categories.any()) + else: + lists_query = lists_query.join( + shopping_list_category, + shopping_list_category.c.shopping_list_id == ShoppingList.id, + ).filter(shopping_list_category.c.category_id == category_id) if start_date_str and end_date_str: try: @@ -1704,7 +1765,7 @@ def user_expenses(): ] return render_template( - "user_expenses.html", + "expenses.html", expense_table=expense_table, lists_data=lists_data, categories=categories, @@ -1713,14 +1774,14 @@ def user_expenses(): ) -@app.route("/user_expenses_data") +@app.route("/expenses_data") @login_required -def user_expenses_data(): +def 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", "true").lower() == "true" - category_id = request.args.get("category_id", type=int) + category_id = request.args.get("category_id") by_category = request.args.get("by_category", "false").lower() == "true" if by_category: @@ -1730,6 +1791,7 @@ def user_expenses_data(): start_date=start_date, end_date=end_date, user_id=current_user.id, + category_id=category_id, ) else: result = get_total_expenses_grouped_by_list_created_at( @@ -2722,30 +2784,6 @@ def delete_suggestion_ajax(suggestion_id): return jsonify({"success": True, "message": "Sugestia została usunięta."}) -@app.route("/admin/expenses_data") -@login_required -def admin_expenses_data(): - if not current_user.is_admin: - return jsonify({"error": "Brak uprawnień"}), 403 - - range_type = request.args.get("range", "monthly") - start_date = request.args.get("start_date") - end_date = request.args.get("end_date") - - result = get_total_expenses_grouped_by_list_created_at( - user_only=False, - admin=True, - show_all=True, - range_type=range_type, - start_date=start_date, - end_date=end_date, - user_id=None, - ) - if "error" in result: - return jsonify({"error": result["error"]}), 400 - return jsonify(result) - - @app.route("/admin/promote_user/") @login_required @admin_required diff --git a/static/js/user_expenses.js b/static/js/expense_chart.js similarity index 71% rename from static/js/user_expenses.js rename to static/js/expense_chart.js index 5740220..2f75974 100644 --- a/static/js/user_expenses.js +++ b/static/js/expense_chart.js @@ -1,11 +1,14 @@ document.addEventListener("DOMContentLoaded", function () { let expensesChart = null; - let selectedCategoryId = ""; - let categorySplit = false; + let categorySplit = true; const rangeLabel = document.getElementById("chartRangeLabel"); - function loadExpenses(range = "monthly", startDate = null, endDate = null) { - let url = '/user_expenses_data?range=' + range; + if (typeof window.selectedCategoryId === "undefined") { + window.selectedCategoryId = ""; + } + + function loadExpenses(range = "last30days", startDate = null, endDate = null) { + let url = '/expenses_data?range=' + range; const showAllCheckbox = document.getElementById("showAllLists"); if (showAllCheckbox && showAllCheckbox.checked) { url += '&show_all=true'; @@ -13,8 +16,8 @@ document.addEventListener("DOMContentLoaded", function () { if (startDate && endDate) { url += `&start_date=${startDate}&end_date=${endDate}`; } - if (selectedCategoryId) { - url += `&category_id=${selectedCategoryId}`; + if (window.selectedCategoryId) { + url += `&category_id=${window.selectedCategoryId}`; } if (categorySplit) { url += '&by_category=true'; @@ -32,23 +35,13 @@ document.addEventListener("DOMContentLoaded", function () { if (categorySplit) { expensesChart = new Chart(ctx, { type: 'bar', - data: { - labels: data.labels, - datasets: data.datasets - }, + data: { labels: data.labels, datasets: data.datasets }, options: { responsive: true, plugins: { tooltip: { mode: 'index', - intersect: false, - callbacks: { - label: function (context) { - const value = context.raw; - if (!value) return null; - return `${context.dataset.label}: ${value}`; - } - } + intersect: false }, legend: { position: 'top' } }, @@ -58,9 +51,7 @@ document.addEventListener("DOMContentLoaded", function () { } } }); - } - else { - // Tryb zwykły + } else { expensesChart = new Chart(ctx, { type: 'bar', data: { @@ -82,20 +73,23 @@ document.addEventListener("DOMContentLoaded", function () { rangeLabel.textContent = `Widok: własny zakres (${startDate} → ${endDate})`; } else { let labelText = ""; - if (range === "monthly") labelText = "Widok: miesięczne"; + if (range === "last30days") labelText = "Widok: ostatnie 30 dni"; + else if (range === "currentmonth") labelText = "Widok: bieżący miesiąc"; + else if (range === "monthly") labelText = "Widok: miesięczne"; else if (range === "quarterly") labelText = "Widok: kwartalne"; else if (range === "halfyearly") labelText = "Widok: półroczne"; else if (range === "yearly") labelText = "Widok: roczne"; rangeLabel.textContent = labelText; } }) - .catch(error => { - console.error("Błąd pobierania danych:", error); - }); + .catch(error => console.error("Błąd pobierania danych:", error)); } - // Obsługa przycisku przełączania trybu - document.getElementById("toggleCategorySplit").addEventListener("click", function () { + // Udostępnienie globalne, żeby inne skrypty mogły wywołać reload + window.loadExpenses = loadExpenses; + + const toggleBtn = document.getElementById("toggleCategorySplit"); + toggleBtn.addEventListener("click", function () { categorySplit = !categorySplit; if (categorySplit) { this.textContent = "🔵 Pokaż całościowo"; @@ -106,9 +100,13 @@ document.addEventListener("DOMContentLoaded", function () { this.classList.remove("btn-outline-info"); this.classList.add("btn-outline-warning"); } - loadExpenses(); // przeładuj wykres + loadExpenses(); }); + toggleBtn.textContent = "🔵 Pokaż całościowo"; + toggleBtn.classList.remove("btn-outline-warning"); + toggleBtn.classList.add("btn-outline-info"); + const startDateInput = document.getElementById("startDate"); const endDateInput = document.getElementById("endDate"); @@ -119,8 +117,6 @@ document.addEventListener("DOMContentLoaded", function () { startDateInput.value = formatDate(lastWeek); endDateInput.value = formatDate(today); - loadExpenses(); - document.getElementById('customRangeBtn').addEventListener('click', function () { const startDate = startDateInput.value; const endDate = endDateInput.value; @@ -141,12 +137,13 @@ document.addEventListener("DOMContentLoaded", function () { }); }); - document.querySelectorAll('.category-filter').forEach(btn => { - btn.addEventListener('click', function () { - document.querySelectorAll('.category-filter').forEach(b => b.classList.remove('active')); - this.classList.add('active'); - selectedCategoryId = this.dataset.categoryId || ""; - loadExpenses(); - }); + // Automatyczne ładowanie danych po przełączeniu na zakładkę Wykres + document.getElementById('chart-tab').addEventListener('shown.bs.tab', function () { + loadExpenses(); }); + + // Jeśli jesteśmy od razu na zakładce Wykres + if (document.getElementById('chart-tab').classList.contains('active')) { + loadExpenses("last30days"); + } }); diff --git a/static/js/expense_tab.js b/static/js/expense_tab.js new file mode 100644 index 0000000..d073077 --- /dev/null +++ b/static/js/expense_tab.js @@ -0,0 +1,11 @@ +document.addEventListener("DOMContentLoaded", function () { + // Sprawdzamy, czy hash w URL to #chartTab + if (window.location.hash === "#chartTab") { + const chartTabTrigger = document.querySelector('#chart-tab'); + if (chartTabTrigger) { + // Wymuszenie aktywacji zakładki Bootstrap + const tab = new bootstrap.Tab(chartTabTrigger); + tab.show(); + } + } +}); diff --git a/static/js/expense_table.js b/static/js/expense_table.js new file mode 100644 index 0000000..c8971cc --- /dev/null +++ b/static/js/expense_table.js @@ -0,0 +1,176 @@ +document.addEventListener('DOMContentLoaded', () => { + const checkboxes = document.querySelectorAll('.list-checkbox'); + const totalEl = document.getElementById('listsTotal'); + const filterButtons = document.querySelectorAll('.range-btn'); + const rows = document.querySelectorAll('#listsTableBody tr'); + const categoryButtons = document.querySelectorAll('.category-filter'); + const onlyWith = document.getElementById('onlyWithExpenses'); + + window.selectedCategoryId = ""; + let initialLoad = true; // flaga - true tylko przy pierwszym wejściu + + 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'; + } + + 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); + } + + function filterByRange(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')}`; + + let startDate = null; + let endDate = null; + + if (range === 'last30days') { + endDate = now; + startDate = new Date(); + startDate.setDate(endDate.getDate() - 29); + } + if (range === 'currentmonth') { + startDate = new Date(year, now.getMonth(), 1); + endDate = now; + } + + rows.forEach(row => { + const rDate = row.dataset.date; + const rMonth = row.dataset.month; + const rWeek = row.dataset.week; + const rYear = row.dataset.year; + const rowDateObj = new Date(rDate); + + let show = true; + if (range === 'day') show = rDate === todayStr; + else if (range === 'month') show = rMonth === month; + else if (range === 'week') show = rWeek === week; + else if (range === 'year') show = rYear === String(year); + else if (range === 'all') show = true; + else if (range === 'last30days') show = rowDateObj >= startDate && rowDateObj <= endDate; + else if (range === 'currentmonth') show = rowDateObj >= startDate && rowDateObj <= endDate; + + row.style.display = show ? '' : 'none'; + }); + } + + function filterByLast30Days() { + filterByRange('last30days'); + } + + 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'; + }); + } + + function applyCategoryFilter() { + if (!window.selectedCategoryId) return; + + rows.forEach(row => { + const categoriesStr = row.dataset.categories || ""; + const categories = categoriesStr ? categoriesStr.split(",") : []; + + if (window.selectedCategoryId === "none") { + // Bez kategorii + if (categoriesStr.trim() !== "") { + row.style.display = 'none'; + } + } else { + // Normalne filtrowanie po ID kategorii + if (!categories.includes(String(window.selectedCategoryId))) { + row.style.display = 'none'; + } + } + }); + } + + // Obsługa checkboxów wierszy + checkboxes.forEach(cb => cb.addEventListener('change', updateTotal)); + + // Obsługa przycisków zakresu + filterButtons.forEach(btn => { + btn.addEventListener('click', () => { + initialLoad = false; // po kliknięciu wyłączamy tryb startowy + + filterButtons.forEach(b => b.classList.remove('active')); + btn.classList.add('active'); + + const range = btn.dataset.range; + filterByRange(range); + applyExpenseFilter(); + applyCategoryFilter(); + updateTotal(); + }); + }); + + // Checkbox "tylko z wydatkami" + if (onlyWith) { + onlyWith.addEventListener('change', () => { + if (initialLoad) { + filterByLast30Days(); + } else { + const activeRange = document.querySelector('.range-btn.active'); + if (activeRange) { + filterByRange(activeRange.dataset.range); + } + } + applyExpenseFilter(); + applyCategoryFilter(); + updateTotal(); + }); + } + + // Obsługa kliknięcia w kategorię + categoryButtons.forEach(btn => { + btn.addEventListener('click', () => { + categoryButtons.forEach(b => b.classList.remove('btn-success', 'active')); + categoryButtons.forEach(b => b.classList.add('btn-outline-light')); + btn.classList.remove('btn-outline-light'); + btn.classList.add('btn-success', 'active'); + + window.selectedCategoryId = btn.dataset.categoryId || ""; + + if (initialLoad) { + filterByLast30Days(); + } else { + const activeRange = document.querySelector('.range-btn.active'); + if (activeRange) { + filterByRange(activeRange.dataset.range); + } + } + + applyExpenseFilter(); + applyCategoryFilter(); + updateTotal(); + + const chartTab = document.querySelector('#chart-tab'); + if (chartTab && chartTab.classList.contains('active') && typeof window.loadExpenses === 'function') { + window.loadExpenses(); + } + }); + }); + + // Start – domyślnie ostatnie 30 dni + filterByLast30Days(); + applyExpenseFilter(); + applyCategoryFilter(); + updateTotal(); +}); diff --git a/static/js/expenses.js b/static/js/expenses.js deleted file mode 100644 index 9c4b338..0000000 --- a/static/js/expenses.js +++ /dev/null @@ -1,93 +0,0 @@ -document.addEventListener("DOMContentLoaded", function () { - let expensesChart = null; - const rangeLabel = document.getElementById("chartRangeLabel"); - - function loadExpenses(range = "monthly", startDate = null, endDate = null) { - let url = '/admin/expenses_data?range=' + range; - if (startDate && endDate) { - url += `&start_date=${startDate}&end_date=${endDate}`; - } - - fetch(url, { cache: "no-store" }) - .then(response => response.json()) - .then(data => { - const ctx = document.getElementById('expensesChart').getContext('2d'); - - if (expensesChart) { - expensesChart.destroy(); - } - - expensesChart = new Chart(ctx, { - type: 'bar', - data: { - labels: data.labels, - datasets: [{ - label: 'Suma wydatków [PLN]', - data: data.expenses, - backgroundColor: '#0d6efd' - }] - }, - options: { - scales: { - y: { - beginAtZero: true - } - } - } - }); - - if (startDate && endDate) { - rangeLabel.textContent = `Widok: własny zakres (${startDate} → ${endDate})`; - } else { - let labelText = ""; - if (range === "monthly") labelText = "Widok: miesięczne"; - else if (range === "quarterly") labelText = "Widok: kwartalne"; - else if (range === "halfyearly") labelText = "Widok: półroczne"; - else if (range === "yearly") labelText = "Widok: roczne"; - rangeLabel.textContent = labelText; - } - - }) - .catch(error => { - console.error("Błąd pobierania danych:", error); - }); - } - - document.getElementById('loadExpensesBtn').addEventListener('click', function () { - loadExpenses(); - }); - - document.querySelectorAll('.range-btn').forEach(btn => { - btn.addEventListener('click', function () { - document.querySelectorAll('.range-btn').forEach(b => b.classList.remove('active')); - this.classList.add('active'); - const range = this.getAttribute('data-range'); - loadExpenses(range); - }); - }); - - document.getElementById('customRangeBtn').addEventListener('click', function () { - const startDate = document.getElementById('startDate').value; - const endDate = document.getElementById('endDate').value; - if (startDate && endDate) { - document.querySelectorAll('.range-btn').forEach(b => b.classList.remove('active')); - loadExpenses('custom', startDate, endDate); - } else { - alert("Proszę wybrać obie daty!"); - } - }); -}); - -document.addEventListener("DOMContentLoaded", function () { - const startDateInput = document.getElementById("startDate"); - const endDateInput = document.getElementById("endDate"); - - const today = new Date(); - const threeDaysAgo = new Date(today); - threeDaysAgo.setDate(today.getDate() - 7); - - const formatDate = (d) => d.toISOString().split('T')[0]; - - startDateInput.value = formatDate(threeDaysAgo); - endDateInput.value = formatDate(today); -}); \ No newline at end of file diff --git a/static/js/user_expense_category.js b/static/js/user_expense_category.js deleted file mode 100644 index 926ead1..0000000 --- a/static/js/user_expense_category.js +++ /dev/null @@ -1,22 +0,0 @@ -document.addEventListener("DOMContentLoaded", function () { - const categoryButtons = document.querySelectorAll(".category-filter"); - const rows = document.querySelectorAll("#listsTableBody tr"); - - categoryButtons.forEach(btn => { - btn.addEventListener("click", function () { - const selectedCat = this.dataset.category; - - categoryButtons.forEach(b => b.classList.remove("active")); - this.classList.add("active"); - - rows.forEach(row => { - const rowCats = row.dataset.categories ? row.dataset.categories.split(",") : []; - if (selectedCat === "all" || rowCats.includes(selectedCat)) { - row.style.display = ""; - } else { - row.style.display = "none"; - } - }); - }); - }); -}); diff --git a/static/js/user_expense_lists.js b/static/js/user_expense_lists.js deleted file mode 100644 index 012a3e4..0000000 --- a/static/js/user_expense_lists.js +++ /dev/null @@ -1,158 +0,0 @@ -document.addEventListener('DOMContentLoaded', () => { - const checkboxes = document.querySelectorAll('.list-checkbox'); - const totalEl = document.getElementById('listsTotal'); - const filterButtons = document.querySelectorAll('.range-btn'); - const rows = document.querySelectorAll('#listsTableBody tr'); - - const onlyWith = document.getElementById('onlyWithExpenses'); - const customStart = document.getElementById('customStart'); - const customEnd = document.getElementById('customEnd'); - - if (localStorage.getItem('customStart')) { - customStart.value = localStorage.getItem('customStart'); - } - if (localStorage.getItem('customEnd')) { - customEnd.value = localStorage.getItem('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; - - localStorage.removeItem('customStart'); - localStorage.removeItem('customEnd'); - - 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(); - }); - }); - - 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); - } - - document.getElementById('applyCustomRange').addEventListener('click', () => { - const start = customStart.value; - const end = customEnd.value; - - // Zapamiętaj daty - localStorage.setItem('customStart', start); - localStorage.setItem('customEnd', end); - - 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(); - }); - - 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'; - }); - } - - // Domyślnie kliknij „Miesiąc” - const defaultBtn = document.querySelector('.range-btn[data-range="month"]'); - if (defaultBtn && !customStart.value && !customEnd.value) { - defaultBtn.click(); - } -}); - -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"; - const updateTotalEvent = new Event('change'); - checkboxes.forEach(cb => cb.dispatchEvent(updateTotalEvent)); - }); -}); - -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; - } -}); - -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(); -}); diff --git a/templates/admin/admin_panel.html b/templates/admin/admin_panel.html index 1ab8ed0..aa49152 100644 --- a/templates/admin/admin_panel.html +++ b/templates/admin/admin_panel.html @@ -118,10 +118,10 @@ - + + @@ -219,52 +219,13 @@ - - - {% block scripts %} - - - {% endblock %}
diff --git a/templates/base.html b/templates/base.html index 51f1c50..9dea439 100644 --- a/templates/base.html +++ b/templates/base.html @@ -49,7 +49,7 @@ {% if current_user.is_admin %} ⚙️ {% endif %} - 📊 + 📊 🚪 {% else %} 🔑 Zaloguj diff --git a/templates/user_expenses.html b/templates/expenses.html similarity index 73% rename from templates/user_expenses.html rename to templates/expenses.html index dd44e6a..1d7bef9 100644 --- a/templates/user_expenses.html +++ b/templates/expenses.html @@ -16,30 +16,32 @@
+
- + {% for cat in categories %} - + {% endfor %}
+
- - +
-
- - - - - +
+ + + + + +
- -
Od @@ -75,6 +75,7 @@
+
@@ -89,6 +90,8 @@ ✅ Zaznacz wszystkie
+ +
@@ -104,8 +107,7 @@ - + data-categories="{% if list.categories %}{{ ','.join(list.categories | map('string')) }}{% else %}{% endif %}"> @@ -123,6 +124,7 @@
{{ list.title }}
👤 {{ list.owner_username or '?' }} -
{{ list.created_at.strftime('%Y-%m-%d') }} {{ '%.2f'|format(list.total_expense) }}
+
💰 Suma zaznaczonych: 0.00 PLN
@@ -133,25 +135,26 @@
- -

Widok: miesięczne

- - - - +
+ + + + + + +
-
Od @@ -161,19 +164,16 @@
- -
-
-
+ {% endblock %} {% block scripts %} - - - + + + {% endblock %} \ No newline at end of file