This commit is contained in:
Mateusz Gruszczyński
2025-09-19 10:18:41 +02:00
parent 44c3f8eb5b
commit 69ecc26236
7 changed files with 1279 additions and 318 deletions

File diff suppressed because it is too large Load Diff

416
static/css/style_old.css Normal file
View File

@@ -0,0 +1,416 @@
/* --- Rozmiary i kursory --- */
.large-checkbox {
width: 1.5em;
height: 1.5em;
}
.clickable-item {
cursor: pointer;
}
/* --- Kolory tła (nadpisane klasy Bootstrapa) --- */
.bg-success {
background-color: #1e7e34 !important;
}
.btn-outline-light:hover {
background-color: #ffc107 !important;
color: #000 !important;
border-color: #ffc107 !important;
}
.progress-dark {
background-color: #212529 !important;
border-radius: 20px !important;
overflow: hidden;
}
.progress-bar {
border-radius: 0 !important;
transition: width 0.4s ease, background-color 0.4s ease;
}
.progress-bar:first-child {
border-top-left-radius: 20px !important;
border-bottom-left-radius: 20px !important;
}
.progress-bar:last-child {
border-top-right-radius: 20px !important;
border-bottom-right-radius: 20px !important;
}
/* rodzic już ma position-relative */
.progress-label {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
pointer-events: none;
/* klikalne przyciski obok paska nie ucierpią */
white-space: nowrap;
}
.progress-thin {
height: 12px;
}
.item-not-checked {
background-color: #2c2f33 !important;
color: white !important;
}
/* --- Styl przycisku wyboru pliku --- */
input[type="file"]::file-selector-button {
background-color: #225d36;
color: #fff;
border: none;
padding: 0.5em 1em;
border-radius: 4px;
font-weight: bold;
cursor: pointer;
transition: background 0.2s;
}
/* --- Ciemniejsze alerty Bootstrapa --- */
.alert-success {
background-color: #225d36 !important;
color: #eaffea !important;
border-color: #174428 !important;
}
.alert-danger {
background-color: #7a1f23 !important;
color: #ffeaea !important;
border-color: #531417 !important;
}
.alert-info {
background-color: #1d3a4d !important;
color: #eaf6ff !important;
border-color: #152837 !important;
}
.alert-warning {
background-color: #665c1e !important;
color: #fffbe5 !important;
border-color: #4d4415 !important;
}
/* Badge - kolory pasujące do ciemnych alertów */
.badge.bg-success,
.badge.text-bg-success {
background-color: #225d36 !important;
color: #eaffea !important;
}
.badge.bg-danger,
.badge.text-bg-danger {
background-color: #7a1f23 !important;
color: #ffeaea !important;
}
.badge.bg-info,
.badge.text-bg-info {
background-color: #1d3a4d !important;
color: #eaf6ff !important;
}
.badge.bg-warning,
.badge.text-bg-warning {
background-color: #665c1e !important;
color: #fffbe5 !important;
}
.badge.bg-secondary,
.badge.text-bg-secondary {
background-color: #343a40 !important;
color: #e2e3e5 !important;
}
.badge.bg-primary,
.badge.text-bg-primary {
background-color: #184076 !important;
color: #e6f0ff !important;
}
.badge.bg-light,
.badge.text-bg-light {
background-color: #444950 !important;
color: #f8f9fa !important;
}
.badge.bg-dark,
.badge.text-bg-dark {
background-color: #181a1b !important;
color: #f8f9fa !important;
}
/* --- Styl dla własnych checkboxów --- */
input[type="checkbox"].large-checkbox {
appearance: none;
-webkit-appearance: none;
-moz-appearance: none;
width: 1.5em;
height: 1.5em;
margin: 0;
padding: 0;
outline: none;
background: none;
cursor: pointer;
position: relative;
vertical-align: middle;
}
input[type="checkbox"].large-checkbox::before {
content: '✗';
color: #dc3545;
font-size: 1.5em;
font-weight: bold;
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
line-height: 1;
transition: color 0.2s;
}
input[type="checkbox"].large-checkbox:checked::before {
content: '✓';
color: #ffffff;
}
input[type="checkbox"].large-checkbox:disabled::before {
opacity: 0.5;
cursor: not-allowed;
}
input[type="checkbox"].large-checkbox:disabled {
cursor: not-allowed;
}
#tempToggle {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
input.form-control {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
.info-bar-fixed {
width: 100%;
color: #f8f9fa;
background-color: #212529;
border-radius: 12px 12px 0 0;
text-align: center;
padding: 10px 10px;
font-size: 0.95rem;
box-sizing: border-box;
margin-top: 2rem;
box-shadow: 0 -1px 4px rgba(0, 0, 0, 0.25);
}
@media (max-width: 768px) {
.info-bar-fixed {
position: static;
font-size: 0.85rem;
padding: 8px 4px;
border-radius: 0;
}
}
.table-responsive {
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
.table-responsive table {
min-width: 1000px;
}
.bg-dark .form-control::placeholder {
color: #ccc !important;
opacity: 1;
}
.toast-body {
color: #ffffff !important;
font-weight: 500 !important;
}
.toast {
animation: fadeInUp 0.5s ease;
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
#mass-add-list li.active {
background: #198754 !important;
color: #fff !important;
border: 1px solid #000000 !important;
}
#mass-add-list li {
transition: background 0.2s;
}
.quantity-input {
width: 60px;
background: #343a40;
color: #fff;
border: 1px solid #495057;
border-radius: 4px;
text-align: center;
}
.add-btn {
margin-left: 10px;
}
.quantity-controls {
min-width: 120px;
display: flex;
align-items: center;
justify-content: flex-end;
gap: 4px;
}
.list-group-item {
display: flex;
align-items: center;
justify-content: space-between;
}
#empty-placeholder {
font-style: italic;
pointer-events: none;
}
#items li.hide-purchased {
display: none !important;
}
.list-group-item:first-child,
.list-group-item:last-child {
border-radius: 0 !important;
}
.fade-out {
opacity: 0;
transition: opacity 0.5s ease;
}
@media (pointer: fine) {
.only-mobile {
display: none !important;
}
}
.ts-dropdown .active {
background-color: #495057 !important;
}
.pagination-dark .page-link {
color: #fff;
background-color: #212529;
border: 1px solid #495057;
}
.pagination-dark .page-link:hover {
background-color: #343a40;
border-color: #6c757d;
color: #fff;
}
.pagination-dark .page-item.active .page-link {
background-color: #0d6efd;
border-color: #0d6efd;
color: #fff;
}
.pagination-dark .page-item.disabled .page-link {
background-color: #2b3035;
border-color: #495057;
color: #6c757d;
}
.tom-dark .ts-control {
background-color: #212529 !important;
color: #fff !important;
border: 1px solid #495057 !important;
border-radius: 0.375rem;
min-height: 38px;
padding: 0.25rem 0.5rem;
box-sizing: border-box;
}
.tom-dark .ts-control .item {
background-color: #343a40 !important;
color: #fff !important;
border-radius: 0.25rem;
padding: 2px 8px;
margin-right: 4px;
}
.ts-dropdown {
background-color: #212529 !important;
color: #fff !important;
border: 1px solid #495057;
border-radius: 0.375rem;
z-index: 9999 !important;
max-height: 300px;
overflow-y: auto;
}
.ts-dropdown .active {
background-color: #495057 !important;
color: #fff !important;
}
td select.tom-dark {
width: 100%;
max-width: 100%;
box-sizing: border-box;
}
.table-dark.table-striped tbody tr:nth-of-type(odd) {
background-color: rgba(255, 255, 255, 0.025);
}
.table-dark tbody tr:hover {
background-color: rgba(255, 255, 255, 0.04);
}
.table-dark thead th {
background-color: #1c1f22;
color: #e1e1e1;
font-weight: 500;
border-bottom: 1px solid #3a3f44;
}
.table-dark td,
.table-dark th {
padding: 0.6rem 0.75rem;
vertical-align: middle;
border-top: 1px solid #3a3f44;
}
.card .table {
border-radius: 0 !important;
overflow: hidden;
margin-bottom: 0;
}

View File

@@ -1,58 +0,0 @@
document.addEventListener('DOMContentLoaded', function() {
// Znajdź przyciski
const toggleMonthlySplit = document.getElementById('toggleMonthlySplit');
const toggleDailySplit = document.getElementById('toggleDailySplit');
const toggleCategorySplit = document.getElementById('toggleCategorySplit');
// Funkcja ustawiająca aktywność przycisków podziału czasu
function setActiveTimeSplit(active) {
if (active === 'monthly') {
toggleMonthlySplit.classList.add('btn-primary');
toggleMonthlySplit.classList.remove('btn-outline-light');
toggleMonthlySplit.setAttribute('aria-pressed', 'true');
toggleDailySplit.classList.remove('btn-primary');
toggleDailySplit.classList.add('btn-outline-light');
toggleDailySplit.setAttribute('aria-pressed', 'false');
} else if (active === 'daily') {
toggleDailySplit.classList.add('btn-primary');
toggleDailySplit.classList.remove('btn-outline-light');
toggleDailySplit.setAttribute('aria-pressed', 'true');
toggleMonthlySplit.classList.remove('btn-primary');
toggleMonthlySplit.classList.add('btn-outline-light');
toggleMonthlySplit.setAttribute('aria-pressed', 'false');
}
}
// Obsługa kliknięć przycisków czasu
toggleMonthlySplit.addEventListener('click', function() {
setActiveTimeSplit('monthly');
loadExpenses('monthly');
});
toggleDailySplit.addEventListener('click', function() {
setActiveTimeSplit('daily');
loadExpenses('daily');
});
// Obsługa kliknięcia przycisku podziału kategorii
toggleCategorySplit.addEventListener('click', function() {
const isActive = this.classList.contains('btn-primary');
if (isActive) {
this.classList.remove('btn-primary');
this.classList.add('btn-outline-light');
this.setAttribute('aria-pressed', 'false');
loadExpenses(); // wyłącz podział kategorii
} else {
this.classList.add('btn-primary');
this.classList.remove('btn-outline-light');
this.setAttribute('aria-pressed', 'true');
loadExpenses({ bycategory: true }); // włącz podział kategorii
}
});
// Inicjalizacja - domyślnie ustaw podział dzienny
setActiveTimeSplit('daily');
loadExpenses('daily');
});

View File

@@ -0,0 +1,67 @@
// download_chart.js — eksport PNG z ciemnym tłem (tymczasowo), bez wielokrotnego bindowania
document.addEventListener("DOMContentLoaded", () => {
const dlBtn = document.getElementById("downloadMainChartBtn");
if (!dlBtn) return;
// helper: bezpieczna nazwa pliku
const sanitize = (s) =>
(s || "")
.normalize("NFD").replace(/[\u0300-\u036f]/g, "")
.replace(/[^a-zA-Z0-9-_]+/g, "_")
.replace(/_+/g, "_").replace(/^_+|_+$/g, "");
// helper: eksport z tymczasowym tłem
const exportChartPNG = (chart, bgColor = "#1e1e1e") => {
const canvas = chart.canvas;
const ctx = canvas.getContext("2d");
// 1) zapisz obraz
const snapshot = ctx.getImageData(0, 0, canvas.width, canvas.height);
// 2) podłóż tło pod istniejący rysunek
ctx.save();
ctx.globalCompositeOperation = "destination-over";
ctx.fillStyle = bgColor;
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.restore();
// 3) wygeneruj PNG
const dataUrl = chart.toBase64Image("image/png", 1.0);
// 4) przywróć pierwotny obraz (transparentny)
ctx.putImageData(snapshot, 0, 0);
return dataUrl;
};
// jednorazowe bindowanie click
if (!dlBtn.dataset.bound) {
dlBtn.addEventListener("click", () => {
const chart = window.expensesChart || Chart.getChart(document.getElementById("expensesChart"));
if (!chart) return;
// nazwa: zakres + timestamp
const now = new Date();
const pad = (n) => String(n).padStart(2, "0");
const stamp = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}_${pad(now.getHours())}-${pad(now.getMinutes())}-${pad(now.getSeconds())}`;
const rangeLabel = document.getElementById("chartRangeLabel")?.textContent || "";
const filename = `wydatki-${sanitize(rangeLabel)}-${stamp}.png`;
// (opcjonalnie) upewnij się, że layout jest świeży
chart.resize();
chart.update("none");
const a = document.createElement("a");
a.href = exportChartPNG(chart, "#1e1e1e"); // tu ustawiasz kolor tła eksportu
a.download = filename;
a.click();
});
dlBtn.dataset.bound = "1";
}
// aktywuj przycisk, gdy wykres istnieje
const enableIfReady = () => { dlBtn.disabled = !window.expensesChart; };
document.addEventListener("expensesChart:ready", enableIfReady);
enableIfReady();
});

View File

@@ -89,7 +89,9 @@ document.addEventListener("DOMContentLoaded", function () {
.then((data) => {
if (!ctx) return;
if (expensesChart) expensesChart.destroy();
if (expensesChart) { expensesChart.destroy(); window.expensesChart = null; }
//if (expensesChart) expensesChart.destroy();
const tooltipOptions = {
mode: "index",
@@ -105,6 +107,7 @@ document.addEventListener("DOMContentLoaded", function () {
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 || [] },
options: {
@@ -116,6 +119,7 @@ document.addEventListener("DOMContentLoaded", function () {
} else {
// Całościowo backend zwraca labels + expenses (sumy) :contentReference[oaicite:7]{index=7}
expensesChart = new Chart(ctx, {
type: "bar",
data: {
labels: data.labels || [],
@@ -132,6 +136,10 @@ document.addEventListener("DOMContentLoaded", function () {
});
}
// na potrzeby otwarciu w modalu
window.expensesChart = expensesChart;
document.dispatchEvent(new Event('expensesChart:ready'));
applyRangeLabel(range, startDate, endDate);
})
.catch((e) => console.error("Błąd pobierania danych:", e));

118
static/js/modal_chart.js Normal file
View File

@@ -0,0 +1,118 @@
// modal_chart.js — final: kopiuje kolory z oryginałów, bez fallbacków i bez debugów
function openChartFullscreen(sourceChartIdOrKey, title) {
const modalEl = document.getElementById("chartFullscreenModal");
const canvas = document.getElementById("chartFullscreenCanvas");
const titleEl = document.getElementById("chartModalTitle");
if (titleEl) titleEl.textContent = title || "Wykres";
// Znajdź wykres źródłowy (po elemencie, id Chart.js lub globalu)
const srcEl = document.getElementById(sourceChartIdOrKey);
const srcChart =
(srcEl && Chart.getChart(srcEl)) ||
Chart.getChart(sourceChartIdOrKey) ||
window[sourceChartIdOrKey] ||
window.expensesChart ||
null;
if (!srcChart) {
bootstrap.Modal.getOrCreateInstance(modalEl).show();
return;
}
// Skopiuj labels i datasets 1:1 (tylko bezpieczne klucze, żeby nie przenosić referencji Chart.js)
const safeDataset = (d) => {
const out = {
// dane i opis
label: d.label,
data: Array.isArray(d.data) ? d.data.slice() : [],
type: d.type,
// kolory / styl — dokładnie z oryginału, jeśli były
backgroundColor: d.backgroundColor,
borderColor: d.borderColor,
borderWidth: d.borderWidth,
borderSkipped: d.borderSkipped,
// stacking / kolejność
stack: d.stack,
order: d.order,
// wszystko co może być ważne dla Twoich barów/konfiguracji
parsing: d.parsing,
indexAxis: d.indexAxis,
};
// usuń klucze undefined (Chart.js lubi czyste configi)
Object.keys(out).forEach((k) => out[k] === undefined && delete out[k]);
return out;
};
const freshData = {
labels: Array.isArray(srcChart.data?.labels) ? srcChart.data.labels.slice() : [],
datasets: (srcChart.data?.datasets || []).map(safeDataset),
};
// Typ wykresu z oryginału (np. "bar")
const chartType = (srcChart.config && srcChart.config.type) || "bar";
// Minimalne, bezpieczne opcje: responsywność + stacking + orientacja
const scx = srcChart.config?.options?.scales?.x || {};
const scy = srcChart.config?.options?.scales?.y || {};
const freshOptions = {
responsive: true,
maintainAspectRatio: false,
// jeżeli oryginał miał pion/poziom, zachowaj
indexAxis: srcChart.config?.options?.indexAxis || "x",
// nie kopiujemy całych pluginów (unikamy referencji) — domyślne legend/tooltip są OK
plugins: {},
scales: {
x: { stacked: !!scx.stacked },
y: { stacked: !!scy.stacked, beginAtZero: scy.beginAtZero !== false },
},
};
// Helper: zniszcz wykres na canvasie modala, jeśli istnieje
const destroyOnCanvas = () => {
if (canvas._chartInstance) {
try { canvas._chartInstance.destroy(); } catch { }
canvas._chartInstance = null;
}
const existing = Chart.getChart(canvas);
if (existing) {
try { existing.destroy(); } catch { }
}
};
destroyOnCanvas();
// Po pokazaniu modala twórz wykres (gdy ma już wymiary)
const onShown = () => {
destroyOnCanvas();
const ctx = canvas.getContext("2d");
canvas._chartInstance = new Chart(ctx, {
type: chartType,
data: freshData,
options: freshOptions,
});
// lekki nudge layoutu
requestAnimationFrame(() => {
canvas._chartInstance.resize();
canvas._chartInstance.update();
});
};
const onHidden = () => { destroyOnCanvas(); };
const modal = bootstrap.Modal.getOrCreateInstance(modalEl);
modalEl.addEventListener("shown.bs.modal", onShown, { once: true });
modalEl.addEventListener("hidden.bs.modal", onHidden, { once: true });
modal.show();
}
// Odblokuj ⛶ gdy bazowy wykres gotowy
document.addEventListener("expensesChart:ready", () => {
const b = document.getElementById("openFsBtn");
if (b) b.disabled = false;
});
document.addEventListener("DOMContentLoaded", () => {
const b = document.getElementById("openFsBtn");
if (b && window.expensesChart) b.disabled = false;
});