diff --git a/static/js/chart_controls.js b/static/js/chart_controls.js new file mode 100644 index 0000000..3d63335 --- /dev/null +++ b/static/js/chart_controls.js @@ -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(); +}); diff --git a/static/js/expense_chart.js b/static/js/expense_chart.js index 1af97d4..145d7d0 100644 --- a/static/js/expense_chart.js +++ b/static/js/expense_chart.js @@ -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; }); diff --git a/templates/expenses.html b/templates/expenses.html index 99732e8..8586389 100644 --- a/templates/expenses.html +++ b/templates/expenses.html @@ -133,19 +133,22 @@
-
-
-
Podział według czasu
-
- - +
+
+
Podział według czasu
+
+ + +
+
+
+
Podział według kategorii
+
-
-
Podział według kategorii
- -
-

Widok: miesięczne

@@ -188,5 +191,5 @@ - + {% endblock %} \ No newline at end of file