zmiany w js

This commit is contained in:
Mateusz Gruszczyński
2025-09-18 07:55:15 +02:00
parent e2761584a3
commit 065f67c45e
3 changed files with 256 additions and 173 deletions

151
static/js/chart_controls.js Normal file
View File

@@ -0,0 +1,151 @@
// chart_controls.js
// Logika UI: wybór zakresu, przełączanie dzienny/miesięczny, kategorie, show_all.
// Współpracuje z window.loadExpenses (z expense_chart.js).
document.addEventListener("DOMContentLoaded", function () {
const toggleMonthlySplit = document.getElementById("toggleMonthlySplit");
const toggleDailySplit = document.getElementById("toggleDailySplit");
const toggleCategory = document.getElementById("toggleCategorySplit");
const startDateInput = document.getElementById("startDate");
const endDateInput = document.getElementById("endDate");
const customRangeBtn = document.getElementById("customRangeBtn");
const showAllCheckbox = document.getElementById("showAllLists");
// pomocnicze
const iso = (d) => d.toISOString().split("T")[0];
const today = () => new Date();
const daysAgo = (n) => { const d = new Date(); d.setDate(d.getDate() - n); return d; };
function setActiveTimeSplit(active) {
const on = (btn) => { btn.classList.add("btn-primary"); btn.classList.remove("btn-outline-light"); btn.setAttribute("aria-pressed", "true"); };
const off = (btn) => { btn.classList.remove("btn-primary"); btn.classList.add("btn-outline-light"); btn.setAttribute("aria-pressed", "false"); };
if (active === "monthly") { on(toggleMonthlySplit); off(toggleDailySplit); }
else { on(toggleDailySplit); off(toggleMonthlySplit); }
}
function isDailyActive() { return toggleDailySplit?.classList.contains("btn-primary"); }
// ——— KLUCZOWE: jedno miejsce, które przeładowuje wykres zgodnie z aktualnym trybem ———
function reloadRespectingSplit(preferredRange = null) {
// preferredRange używamy dla przycisków typu monthly/quarterly/halfyearly/yearly
const sd = startDateInput?.value || null;
const ed = endDateInput?.value || null;
if (isDailyActive()) {
// Dzienny ZAWSZE z datami (fallback: ostatnie 30 dni), bo inaczej backend spadnie na monthly
const _sd = sd && ed ? sd : iso(daysAgo(30));
const _ed = sd && ed ? ed : iso(today());
window.loadExpenses("daily", _sd, _ed);
return;
}
// Miesięczny
if (sd && ed) {
window.loadExpenses("monthly", sd, ed);
} else if (preferredRange) {
window.loadExpenses(preferredRange);
} else {
window.loadExpenses("monthly");
}
}
// ——— Przełączniki czasu ———
toggleMonthlySplit?.addEventListener("click", () => {
setActiveTimeSplit("monthly");
reloadRespectingSplit("monthly");
});
toggleDailySplit?.addEventListener("click", () => {
setActiveTimeSplit("daily");
reloadRespectingSplit();
});
// ——— Podział na kategorie ———
toggleCategory?.addEventListener("click", function () {
const active = this.classList.contains("btn-primary");
if (active) {
this.classList.remove("btn-primary");
this.classList.add("btn-outline-light");
this.setAttribute("aria-pressed", "false");
this.textContent = "Pokaż podział na kategorie";
window.setCategorySplit(false);
} else {
this.classList.add("btn-primary");
this.classList.remove("btn-outline-light");
this.setAttribute("aria-pressed", "true");
this.textContent = "Pokaż całościowo";
window.setCategorySplit(true);
}
reloadRespectingSplit();
});
// ——— Własny zakres ———
customRangeBtn?.addEventListener("click", function () {
const sd = startDateInput?.value;
const ed = endDateInput?.value;
if (!(sd && ed)) return alert("Proszę wybrać obie daty!");
reloadRespectingSplit();
});
// ——— Predefiniowane zakresy pod wykresem ———
document.querySelectorAll("#chartTab .range-btn").forEach((btn) => {
btn.addEventListener("click", function () {
document.querySelectorAll("#chartTab .range-btn").forEach((b) => b.classList.remove("active"));
this.classList.add("active");
const r = this.getAttribute("data-range"); // last30days/currentmonth/monthly/quarterly/halfyearly/yearly
if (r === "currentmonth") {
const t = today();
const first = new Date(t.getFullYear(), t.getMonth(), 1);
if (isDailyActive()) {
window.loadExpenses("daily", iso(first), iso(t));
} else {
window.loadExpenses("monthly", iso(first), iso(t));
}
return;
}
if (r === "last30days") {
if (isDailyActive()) {
window.loadExpenses("daily", iso(daysAgo(30)), iso(today()));
} else {
window.loadExpenses("last30days");
}
return;
}
// monthly/quarterly/halfyearly/yearly kubełkowanie po stronie backendu
reloadRespectingSplit(r);
});
});
// ——— KATEGORIE (🌐 Wszystkie + pojedyncze) ———
document.querySelectorAll(".category-filter").forEach((btn) => {
btn.addEventListener("click", function () {
// UI: podmień podświetlenie
document.querySelectorAll(".category-filter").forEach(b => {
b.classList.remove("btn-success");
b.classList.add("btn-outline-light");
});
this.classList.add("btn-success");
this.classList.remove("btn-outline-light");
// Zapisz filtr kategorii do globalnej zmiennej, którą odczytuje expense_chart.js
const cid = this.getAttribute("data-category-id") || "";
window.selectedCategoryId = cid;
// I ważne: przeładuj zgodnie z aktualnym trybem (to naprawia Twój przypadek #1)
reloadRespectingSplit();
});
});
// ——— SHOW ALL (Uwzględnij listy udostępnione/publiczne) ———
showAllCheckbox?.addEventListener("change", () => {
reloadRespectingSplit();
});
// ——— Inicjalizacja ———
// Podpowiedź dat do inputów
if (startDateInput && endDateInput) {
startDateInput.value = iso(daysAgo(7));
endDateInput.value = iso(today());
}
setActiveTimeSplit("daily");
reloadRespectingSplit();
});

View File

@@ -1,214 +1,143 @@
document.addEventListener("DOMContentLoaded", function () {
// expense_chart.js
// Czyste generowanie wykresu + publiczne API: window.loadExpenses, window.setCategorySplit
// Współpracuje z backendem /expenses_data (range_type, start/end, by_category) patrz app.py :contentReference[oaicite:3]{index=3}
document.addEventListener("DOMContentLoaded", function () {
let expensesChart = null;
let categorySplit = true;
let categorySplit = false; // domyślnie wykres całościowy; przycisk w HTML startuje z aria-pressed="false"
const rangeLabel = document.getElementById("chartRangeLabel");
const showAllCheckbox = document.getElementById("showAllLists");
const ctx = document.getElementById("expensesChart")?.getContext("2d");
// Pomocnicze
const iso = (d) => d.toISOString().split("T")[0];
const today = () => new Date();
const daysAgo = (n) => { const d = new Date(); d.setDate(d.getDate() - n); return d; };
// Jeśli ktoś nie wstrzyknął globalnie selectedCategoryId (np. przez inny widok),
// zapewniamy istnienie zmiennej:
if (typeof window.selectedCategoryId === "undefined") {
window.selectedCategoryId = "";
}
function loadExpenses(range = "currentmonth", startDate = null, endDate = null) {
let url = '/expenses_data?range=' + range;
// Ustawia tryb podziału na kategorie, bez odświeżania (kontroler zadzwoni potem w loadExpenses)
function setCategorySplit(on) {
categorySplit = !!on;
}
const showAllCheckbox = document.getElementById("showAllLists");
// Budowa URL dla /expenses_data zgodnie z backendem (range/start/end/show_all/category_id/by_category) :contentReference[oaicite:4]{index=4}
function buildUrl(range, startDate, endDate) {
let url = `/expenses_data?range=${encodeURIComponent(range)}`;
// show_all
if (showAllCheckbox) {
url += showAllCheckbox.checked ? '&show_all=true' : '&show_all=false';
url += showAllCheckbox.checked ? "&show_all=true" : "&show_all=false";
} else {
url += '&show_all=true';
url += "&show_all=true";
}
// daty (dodaj tylko, gdy kompletne)
if (startDate && endDate) {
url += `&start_date=${startDate}&end_date=${endDate}`;
url += `&start_date=${encodeURIComponent(startDate)}&end_date=${encodeURIComponent(endDate)}`;
}
// filtr kategorii list (z listy, nie "podziału na kategorie" na wykresie)
if (window.selectedCategoryId) {
url += `&category_id=${window.selectedCategoryId}`;
url += `&category_id=${encodeURIComponent(window.selectedCategoryId)}`;
}
// podział na kategorie na wykresie
if (categorySplit) {
url += '&by_category=true';
url += "&by_category=true";
}
return url;
}
// Label dla UI
function applyRangeLabel(range, startDate, endDate) {
if (startDate && endDate) {
rangeLabel.textContent = `Widok: własny zakres (${startDate}${endDate})`;
return;
}
const map = {
last30days: "Widok: ostatnie 30 dni",
currentmonth: "Widok: bieżący miesiąc",
monthly: "Widok: miesięczne",
quarterly: "Widok: kwartalne",
halfyearly: "Widok: półroczne",
yearly: "Widok: roczne",
daily: "Widok: dzienne",
};
rangeLabel.textContent = map[range] || "Widok: miesięczne";
}
// Publiczne API kontroler zawsze woła nas z odpowiednim 'range' i (dla daily) z datami.
// Dla odporności: jeśli przyjdzie 'daily' BEZ dat, wymusimy ostatnie 30 dni (to była usterka źródłowa) :contentReference[oaicite:5]{index=5}
function loadExpenses(range = "monthly", startDate = null, endDate = null) {
// Naprawa: daily bez dat => ostatnie 30 dni
if (range === "daily" && !(startDate && endDate)) {
startDate = iso(daysAgo(30));
endDate = iso(today());
}
const url = buildUrl(range, startDate, endDate);
fetch(url, { cache: "no-store" })
.then(response => response.json())
.then(data => {
const ctx = document.getElementById('expensesChart').getContext('2d');
.then((r) => r.json())
.then((data) => {
if (!ctx) return;
if (expensesChart) {
expensesChart.destroy();
}
if (expensesChart) expensesChart.destroy();
const tooltipOptions = {
mode: 'index',
mode: "index",
intersect: false,
callbacks: {
label: function (context) {
if (context.parsed.y === 0) {
return ''; // pomija kategorie o wartości 0
}
return context.dataset.label + ': ' + context.parsed.y;
}
}
if (context.parsed.y === 0) return "";
return (context.dataset.label || "Suma") + ": " + context.parsed.y;
},
},
};
if (categorySplit) {
// Stacked per-kategoria backend zwraca datasets z labelami kategorii :contentReference[oaicite:6]{index=6}
expensesChart = new Chart(ctx, {
type: 'bar',
data: { labels: data.labels, datasets: data.datasets },
type: "bar",
data: { labels: data.labels || [], datasets: data.datasets || [] },
options: {
responsive: true,
plugins: {
tooltip: tooltipOptions,
legend: { position: 'top' }
},
scales: {
x: { stacked: true },
y: { stacked: true, beginAtZero: true }
}
}
plugins: { tooltip: tooltipOptions, legend: { position: "top" } },
scales: { x: { stacked: true }, y: { stacked: true, beginAtZero: true } },
},
});
} else {
// Całościowo backend zwraca labels + expenses (sumy) :contentReference[oaicite:7]{index=7}
expensesChart = new Chart(ctx, {
type: 'bar',
type: "bar",
data: {
labels: data.labels,
labels: data.labels || [],
datasets: [{
label: 'Suma wydatków [PLN]',
data: data.expenses,
backgroundColor: '#0d6efd'
}]
label: "Suma wydatków [PLN]",
data: data.expenses || [],
}],
},
options: {
responsive: true,
plugins: {
tooltip: tooltipOptions
},
scales: { y: { beginAtZero: true } }
}
plugins: { tooltip: tooltipOptions },
scales: { y: { beginAtZero: true } },
},
});
}
if (startDate && endDate) {
rangeLabel.textContent = `Widok: własny zakres (${startDate}${endDate})`;
} else {
let labelText = "";
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;
}
applyRangeLabel(range, startDate, endDate);
})
.catch(error => console.error("Błąd pobierania danych:", error));
.catch((e) => console.error("Błąd pobierania danych:", e));
}
// Udostępnienie globalne, żeby inne skrypty mogły wywołać reload
// Eksport publiczny dla kontrolerów
window.loadExpenses = loadExpenses;
const toggleBtn = document.getElementById("toggleCategorySplit");
toggleBtn.addEventListener("click", function () {
categorySplit = !categorySplit;
const startDateInput = document.getElementById("startDate");
const endDateInput = document.getElementById("endDate");
const startDate = startDateInput ? startDateInput.value : null;
const endDate = endDateInput ? endDateInput.value : null;
// Wybierz zakres - przy daily ustawiamy range = 'custom', by respektować daty
let activeRange = 'currentmonth'; // default
if (document.getElementById('toggleDailySplit').classList.contains('btn-primary')) {
activeRange = (startDate && endDate) ? 'custom' : 'daily';
} else if (document.getElementById('toggleMonthlySplit').classList.contains('btn-primary')) {
activeRange = 'monthly';
}
if (categorySplit) {
this.textContent = "🔵 Pokaż całościowo";
this.classList.remove("btn-outline-warning");
this.classList.add("btn-outline-info");
} else {
this.textContent = "🎨 Pokaż podział na kategorie";
this.classList.remove("btn-outline-info");
this.classList.add("btn-outline-warning");
}
if (startDate && endDate) {
loadExpenses(activeRange, startDate, endDate);
} else {
loadExpenses(activeRange);
}
});
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");
const today = new Date();
const lastWeek = new Date(today);
lastWeek.setDate(today.getDate() - 7);
const formatDate = (d) => d.toISOString().split('T')[0];
startDateInput.value = formatDate(lastWeek);
endDateInput.value = formatDate(today);
document.getElementById('customRangeBtn').addEventListener('click', function () {
const startDate = startDateInput.value;
const endDate = endDateInput.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.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');
if (range === "currentmonth") {
const today = new Date();
const firstDay = new Date(today.getFullYear(), today.getMonth(), 1);
loadExpenses('custom', formatDate(firstDay), formatDate(today));
} else {
loadExpenses(range);
}
});
});
// Automatyczne ładowanie danych po przełączeniu na zakładkę Wykres
document.getElementById('chart-tab').addEventListener('shown.bs.tab', function () {
// Sprawdź jaki tryb jest aktywny (domyślnie dzienny lub miesięczny)
if (document.getElementById('toggleDailySplit').classList.contains('btn-primary')) {
const startDate = startDateInput.value || null;
const endDate = endDateInput.value || null;
if (startDate && endDate) {
loadExpenses('daily', startDate, endDate);
} else {
loadExpenses('daily');
}
} else if (document.getElementById('toggleMonthlySplit').classList.contains('btn-primary')) {
loadExpenses('monthly');
} else {
loadExpenses('currentmonth'); // Albo inna domyślna wartość
}
});
// Jeśli jesteśmy od razu na zakładce Wykres
//if (document.getElementById('chart-tab').classList.contains('active')) {
// loadExpenses("currentmonth");
//}
window.setCategorySplit = setCategorySplit;
});

View File

@@ -133,19 +133,22 @@
<div class="card bg-dark text-white mb-4">
<div class="card-body">
<div class="d-flex gap-3 mb-3">
<div>
<h6 class="text-white">Podział według czasu</h6>
<div class="btn-group" role="group" aria-label="Podział czasu">
<button type="button" class="btn btn-outline-light" id="toggleMonthlySplit" aria-pressed="true">Pokaż podział miesięczny</button>
<button type="button" class="btn btn-outline-light" id="toggleDailySplit" aria-pressed="false">Pokaż podział dzienny</button>
<div class="d-flex gap-3 mb-3">
<div>
<h6 class="text-white">Podział według czasu</h6>
<div class="btn-group" role="group" aria-label="Podział czasu">
<button type="button" class="btn btn-outline-light" id="toggleMonthlySplit" aria-pressed="true">Pokaż
podział miesięczny</button>
<button type="button" class="btn btn-outline-light" id="toggleDailySplit" aria-pressed="false">Pokaż
podział dzienny</button>
</div>
</div>
<div>
<h6 class="text-white">Podział według kategorii</h6>
<button class="btn btn-outline-light" id="toggleCategorySplit" aria-pressed="false">Pokaż podział na
kategorie</button>
</div>
</div>
<div>
<h6 class="text-white">Podział według kategorii</h6>
<button class="btn btn-outline-light" id="toggleCategorySplit" aria-pressed="false">Pokaż podział na kategorie</button>
</div>
</div>
<p id="chartRangeLabel" class="fw-bold mb-3">Widok: miesięczne</p>
@@ -188,5 +191,5 @@
<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>
<script src="{{ url_for('static_bp.serve_js', filename='select_all_table.js') }}"></script>
<script src="{{ url_for('static_bp.serve_js', filename='chart_split_controls.js') }}"></script>
<script src="{{ url_for('static_bp.serve_js', filename='chart_controls.js') }}"></script>
{% endblock %}