rozbudowa wykresow o kategorie i usuniecie dupliakcji kodu z apnelu admina
This commit is contained in:
19
.env.example
19
.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/)
|
||||
|
142
app.py
142
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/<int:user_id>")
|
||||
@login_required
|
||||
@admin_required
|
||||
|
@@ -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
11
static/js/expense_tab.js
Normal 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
176
static/js/expense_table.js
Normal 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();
|
||||
});
|
@@ -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);
|
||||
});
|
@@ -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";
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@@ -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();
|
||||
});
|
@@ -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">
|
||||
|
@@ -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>
|
||||
|
@@ -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 %}
|
Reference in New Issue
Block a user