rozbudowa wykresow o kategorie i usuniecie dupliakcji kodu z apnelu admina

This commit is contained in:
Mateusz Gruszczyński
2025-08-01 11:31:17 +02:00
parent 2df64bbe2e
commit cfae8571de
11 changed files with 358 additions and 451 deletions

View File

@@ -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/)

142
app.py
View File

@@ -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/<int:user_id>")
@login_required
@admin_required

View File

@@ -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");
}
});

11
static/js/expense_tab.js Normal file
View File

@@ -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();
}
}
});

176
static/js/expense_table.js Normal file
View File

@@ -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();
});

View File

@@ -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);
});

View File

@@ -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";
}
});
});
});
});

View File

@@ -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();
});

View File

@@ -118,10 +118,10 @@
</tbody>
</table>
<button type="button" class="btn btn-outline-primary w-100 mt-3" data-bs-toggle="modal"
data-bs-target="#expensesChartModal" id="loadExpensesBtn">
<a href="{{ url_for('expenses') }}#chartTab" class="btn btn-outline-info">
📊 Pokaż wykres wydatków
</button>
</a>
</div>
</div>
</div>
@@ -219,52 +219,13 @@
</div>
</div>
<div class="modal fade" id="expensesChartModal" tabindex="-1" aria-labelledby="expensesChartModalLabel"
aria-hidden="true">
<div class="modal-dialog modal-xl modal-dialog-centered">
<div class="modal-content bg-dark text-white rounded">
<div class="modal-header border-0">
<div>
<h5 class="modal-title m-0" id="expensesChartModalLabel">📊 Wydatki</h5>
<small id="chartRangeLabel" class="text-muted">Widok: miesięczne</small>
</div>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Zamknij"></button>
</div>
<div class="modal-body pt-0">
<div class="d-flex flex-wrap gap-2 mb-3">
<button class="btn btn-outline-light btn-sm range-btn active" data-range="monthly">📅 Miesięczne</button>
<button class="btn btn-outline-light btn-sm range-btn" data-range="quarterly">📊 Kwartalne</button>
<button class="btn btn-outline-light btn-sm range-btn" data-range="halfyearly">🗓️ Półroczne</button>
<button class="btn btn-outline-light btn-sm range-btn" data-range="yearly">📆 Roczne</button>
</div>
<div class="input-group input-group-sm mb-3 w-100" style="max-width: 570px;">
<span class="input-group-text bg-secondary text-white border-secondary">Od</span>
<input type="date" class="form-control bg-dark text-white border-secondary flex-grow-1" id="startDate">
<span class="input-group-text bg-secondary text-white border-secondary">Do</span>
<input type="date" class="form-control bg-dark text-white border-secondary flex-grow-1" id="endDate">
<button class="btn btn-outline-success" id="customRangeBtn">Pokaż dane z zakresu 📅</button>
</div>
<div class="bg-dark rounded p-2">
<canvas id="expensesChart" height="100"></canvas>
</div>
</div>
</div>
</div>
</div>
{% block scripts %}
<script src="{{ url_for('static_bp.serve_js_lib', filename='chart.js') }}"></script>
<script>
document.getElementById('select-all').addEventListener('click', function () {
const checkboxes = document.querySelectorAll('input[name="list_ids"]');
checkboxes.forEach(cb => cb.checked = this.checked);
});
</script>
<script src="{{ url_for('static_bp.serve_js', filename='expenses.js') }}"></script>
{% endblock %}
<div class="info-bar-fixed">

View File

@@ -49,7 +49,7 @@
{% if current_user.is_admin %}
<a href="{{ url_for('admin_panel') }}" class="btn btn-outline-light btn-sm">⚙️</a>
{% endif %}
<a href="{{ url_for('user_expenses') }}" class="btn btn-outline-light btn-sm">📊</a>
<a href="{{ url_for('expenses') }}" class="btn btn-outline-light btn-sm">📊</a>
<a href="{{ url_for('logout') }}" class="btn btn-outline-light btn-sm">🚪</a>
{% else %}
<a href="{{ url_for('login') }}" class="btn btn-outline-light btn-sm">🔑 Zaloguj</a>

View File

@@ -16,30 +16,32 @@
</div>
</div>
<!-- Przyciski kategorii -->
<div class="d-flex flex-wrap gap-2 mb-3 justify-content-center">
<a href="{{ url_for('user_expenses') }}"
class="btn btn-sm {% if not selected_category %}btn-success{% else %}btn-outline-light{% endif %}">
<button type="button"
class="btn btn-sm category-filter {% if not selected_category %}btn-success{% else %}btn-outline-light{% endif %}"
data-category-id="">
🌐 Wszystkie
</a>
</button>
{% for cat in categories %}
<a href="{{ url_for('user_expenses', category_id=cat.id) }}"
class="btn btn-sm {% if selected_category == cat.id %}btn-success{% else %}btn-outline-light{% endif %}">
<button type="button"
class="btn btn-sm category-filter {% if selected_category == cat.id %}btn-success{% else %}btn-outline-light{% endif %}"
data-category-id="{{ cat.id }}">
{{ cat.name }}
</a>
</button>
{% endfor %}
</div>
<div class="card bg-dark text-white mb-5">
<div class="card-body">
<ul class="nav nav-tabs mb-3" id="expenseTabs" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="lists-tab" data-bs-toggle="tab" data-bs-target="#listsTab" type="button"
role="tab">
📚 Listy
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="chart-tab" data-bs-toggle="tab" data-bs-target="#chartTab" type="button"
role="tab">
@@ -49,22 +51,20 @@
</ul>
<div class="tab-content" id="expenseTabsContent">
<!-- LISTY -->
<!-- LISTY -->
<div class="tab-pane fade show active" id="listsTab" role="tabpanel">
<div class="card bg-dark text-white mb-4">
<div class="card-body">
<div class="d-flex flex-wrap gap-2 mb-3 justify-content-center">
<button class="btn btn-outline-light btn-sm range-btn" data-range="day">🗓️ Dzień</button>
<button class="btn btn-outline-light btn-sm range-btn" data-range="week">📆 Tydzień</button>
<button class="btn btn-outline-light btn-sm range-btn active" data-range="month">📅 Miesiąc</button>
<button class="btn btn-outline-light btn-sm range-btn" data-range="year">📈 Rok</button>
<button class="btn btn-outline-light btn-sm range-btn" data-range="all">🌐 Wszystko</button>
<div class="btn-group btn-group-sm" role="group">
<button class="btn btn-outline-light range-btn" data-range="day">🗓️ Dzień</button>
<button class="btn btn-outline-light range-btn" data-range="week">📆 Tydzień</button>
<button class="btn btn-outline-light range-btn active" data-range="month">📅 Miesiąc</button>
<button class="btn btn-outline-light range-btn" data-range="year">📈 Rok</button>
<button class="btn btn-outline-light range-btn" data-range="all">🌐 Wszystko</button>
</div>
</div>
<div class="d-flex justify-content-center mb-3">
<div class="input-group input-group-sm w-100" style="max-width: 570px;">
<span class="input-group-text bg-secondary text-white border-secondary">Od</span>
@@ -75,6 +75,7 @@
<button class="btn btn-outline-success" id="applyCustomRange">📊 Zastosuj zakres</button>
</div>
</div>
<div class="d-flex justify-content-center mb-3">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="onlyWithExpenses">
@@ -89,6 +90,8 @@
✅ Zaznacz wszystkie
</button>
</div>
<!-- Tabela list z możliwością filtrowania -->
<div class="table-responsive">
<table class="table table-dark table-striped align-middle sortable">
<thead>
@@ -104,8 +107,7 @@
<tr data-date="{{ list.created_at.strftime('%Y-%m-%d') }}"
data-week="{{ list.created_at.isocalendar()[0] }}-{{ '%02d' % list.created_at.isocalendar()[1] }}"
data-month="{{ list.created_at.strftime('%Y-%m') }}" data-year="{{ list.created_at.year }}"
data-categories="{{ ','.join(list.categories | map('string')) }}">
data-categories="{% if list.categories %}{{ ','.join(list.categories | map('string')) }}{% else %}{% endif %}">
<td>
<input type="checkbox" class="form-check-input list-checkbox"
@@ -114,7 +116,6 @@
<td>
<strong>{{ list.title }}</strong>
<br><small class="text-small">👤 {{ list.owner_username or '?' }}</small>
</td>
<td>{{ list.created_at.strftime('%Y-%m-%d') }}</td>
<td>{{ '%.2f'|format(list.total_expense) }}</td>
@@ -123,6 +124,7 @@
</tbody>
</table>
</div>
<hr>
<h5 class="text-success mt-3">💰 Suma zaznaczonych: <span id="listsTotal">0.00 PLN</span></h5>
</div>
@@ -133,25 +135,26 @@
<div class="tab-pane fade" id="chartTab" role="tabpanel">
<div class="card bg-dark text-white mb-4">
<div class="card-body">
<button class="btn btn-outline-light w-100 py-2 mb-2 d-flex align-items-center justify-content-center gap-2"
id="toggleCategorySplit">
🎨 Pokaż podział na kategorie
</button>
<p id="chartRangeLabel" class="fw-bold mb-3">Widok: miesięczne</p>
<canvas id="expensesChart" height="120"></canvas>
</div>
</div>
<div class="d-flex flex-wrap gap-2 mb-3 justify-content-center">
<button class="btn btn-outline-light btn-sm range-btn active" data-range="monthly">📅 Miesięczne</button>
<button class="btn btn-outline-light btn-sm range-btn" data-range="quarterly">📊 Kwartalne</button>
<button class="btn btn-outline-light btn-sm range-btn" data-range="halfyearly">🗓️ Półroczne</button>
<button class="btn btn-outline-light btn-sm range-btn" data-range="yearly">📈 Roczne</button>
<div class="btn-group btn-group-sm" role="group">
<button class="btn btn-outline-light range-btn active" data-range="last30days">🗓️ Ostatnie 30 dni</button>
<button class="btn btn-outline-light range-btn" data-range="currentmonth">📅 Bieżący miesiąc</button>
<button class="btn btn-outline-light range-btn" data-range="monthly">📆 Miesięczne</button>
<button class="btn btn-outline-light range-btn" data-range="quarterly">📊 Kwartalne</button>
<button class="btn btn-outline-light range-btn" data-range="halfyearly">🗓️ Półroczne</button>
<button class="btn btn-outline-light range-btn" data-range="yearly">📈 Roczne</button>
</div>
</div>
<!-- Picker daty w formie input-group -->
<div class="d-flex justify-content-center mb-4">
<div class="input-group input-group-sm w-100" style="max-width: 570px;">
<span class="input-group-text bg-secondary text-white border-secondary">Od</span>
@@ -161,19 +164,16 @@
<button class="btn btn-outline-success" id="customRangeBtn">📊 Pokaż dane z zakresu</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script src="{{ url_for('static_bp.serve_js_lib', filename='chart.js') }}"></script>
<script src="{{ url_for('static_bp.serve_js', filename='user_expenses.js') }}"></script>
<script src="{{ url_for('static_bp.serve_js', filename='user_expense_lists.js') }}"></script>
<script src="{{ url_for('static_bp.serve_js', filename='user_expense_category.js') }}"></script>
<script src="{{ url_for('static_bp.serve_js', filename='expense_chart.js') }}"></script>
<script src="{{ url_for('static_bp.serve_js', filename='expense_table.js') }}"></script>
<script src="{{ url_for('static_bp.serve_js', filename='expense_tab.js') }}"></script>
{% endblock %}