sortowanie_w_mass_add #10

Merged
gru merged 90 commits from sortowanie_w_mass_add into master 2025-09-02 17:08:55 +02:00
23 changed files with 1558 additions and 986 deletions

3
.gitattributes vendored Normal file
View File

@@ -0,0 +1,3 @@
*.py text working-tree-encoding=UTF-8
*.env.example text working-tree-encoding=UTF-8
.env text working-tree-encoding=UTF-8

6
.gitignore vendored
View File

@@ -3,9 +3,9 @@ venv
env
*.db
__pycache__
instance/
database/
uploads/
.DS_Store
db/*
db/mysql/*
db/pgsql/*
db/shopping.db
*.swp

889
app.py

File diff suppressed because it is too large Load Diff

View File

@@ -7,12 +7,16 @@ document.addEventListener("DOMContentLoaded", function () {
window.selectedCategoryId = "";
}
function loadExpenses(range = "last30days", startDate = null, endDate = null) {
function loadExpenses(range = "currentmonth", startDate = null, endDate = null) {
let url = '/expenses_data?range=' + range;
const showAllCheckbox = document.getElementById("showAllLists");
if (showAllCheckbox && showAllCheckbox.checked) {
if (showAllCheckbox) {
url += showAllCheckbox.checked ? '&show_all=true' : '&show_all=false';
} else {
url += '&show_all=true';
}
if (startDate && endDate) {
url += `&start_date=${startDate}&end_date=${endDate}`;
}
@@ -165,6 +169,6 @@ document.addEventListener("DOMContentLoaded", function () {
// Jeśli jesteśmy od razu na zakładce Wykres
if (document.getElementById('chart-tab').classList.contains('active')) {
loadExpenses("last30days");
loadExpenses("currentmonth");
}
});

View File

@@ -4,10 +4,21 @@ document.addEventListener('DOMContentLoaded', () => {
const filterButtons = document.querySelectorAll('.range-btn');
const rows = document.querySelectorAll('#listsTableBody tr');
const categoryButtons = document.querySelectorAll('.category-filter');
const onlyWith = document.getElementById('onlyWithExpenses');
const applyCustomBtn = document.getElementById('applyCustomRange');
const customStartInput = document.getElementById('customStart');
const customEndInput = document.getElementById('customEnd');
if (customStartInput && customEndInput) {
const now = new Date();
const y = now.getFullYear();
const m = String(now.getMonth() + 1).padStart(2, '0');
const d = String(now.getDate()).padStart(2, '0');
customStartInput.value = `${y}-${m}-01`;
customEndInput.value = `${y}-${m}-${d}`;
}
window.selectedCategoryId = "";
let initialLoad = true; // flaga - true tylko przy pierwszym wejściu
let initialLoad = true;
function updateTotal() {
let total = 0;
@@ -35,10 +46,8 @@ document.addEventListener('DOMContentLoaded', () => {
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();
@@ -48,14 +57,12 @@ document.addEventListener('DOMContentLoaded', () => {
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;
@@ -64,7 +71,6 @@ document.addEventListener('DOMContentLoaded', () => {
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';
});
}
@@ -74,7 +80,6 @@ document.addEventListener('DOMContentLoaded', () => {
}
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';
@@ -83,36 +88,36 @@ document.addEventListener('DOMContentLoaded', () => {
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';
}
if (categoriesStr.trim() !== "") row.style.display = 'none';
} else {
// Normalne filtrowanie po ID kategorii
if (!categories.includes(String(window.selectedCategoryId))) {
row.style.display = 'none';
}
if (!categories.includes(String(window.selectedCategoryId))) row.style.display = 'none';
}
});
}
// Obsługa checkboxów wierszy
function filterByCustomRange(startStr, endStr) {
const start = new Date(startStr);
const end = new Date(endStr);
if (isNaN(start) || isNaN(end)) return;
end.setHours(23, 59, 59, 999);
rows.forEach(row => {
const rowDateObj = new Date(row.dataset.date);
const show = rowDateObj >= start && rowDateObj <= end;
row.style.display = show ? '' : 'none';
});
}
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
initialLoad = false;
filterButtons.forEach(b => b.classList.remove('active'));
btn.classList.add('active');
const range = btn.dataset.range;
filterByRange(range);
applyExpenseFilter();
@@ -121,46 +126,22 @@ document.addEventListener('DOMContentLoaded', () => {
});
});
// 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);
}
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();
@@ -168,7 +149,23 @@ document.addEventListener('DOMContentLoaded', () => {
});
});
// Start domyślnie ostatnie 30 dni
if (applyCustomBtn) {
applyCustomBtn.addEventListener('click', () => {
const startStr = customStartInput?.value;
const endStr = customEndInput?.value;
if (!startStr || !endStr) {
alert('Proszę wybrać obie daty!');
return;
}
initialLoad = false;
document.querySelectorAll('.range-btn').forEach(b => b.classList.remove('active'));
filterByCustomRange(startStr, endStr);
applyExpenseFilter();
applyCategoryFilter();
updateTotal();
});
}
filterByLast30Days();
applyExpenseFilter();
applyCategoryFilter();

View File

@@ -231,7 +231,7 @@ function toggleVisibility(listId) {
copyBtn.disabled = false;
toggleBtn.innerHTML = '🙈 Ukryj listę';
} else {
shareHeader.textContent = '🙈 Lista jest ukryta przed gośćmi';
shareHeader.textContent = '🙈 Lista jest ukryta. Link udostępniania nie zadziała!';
shareUrlSpan.style.display = 'none';
copyBtn.disabled = true;
toggleBtn.innerHTML = '👁️ Udostępnij ponownie';

View File

@@ -1,116 +1,225 @@
document.addEventListener('DOMContentLoaded', function () {
const modal = document.getElementById('massAddModal');
const productList = document.getElementById('mass-add-list');
const sortBar = document.getElementById('sort-bar');
const productCountDisplay = document.getElementById('product-count');
const modalBody = modal?.querySelector('.modal-body');
// Funkcja normalizacji (usuwa diakrytyki i zamienia na lowercase)
function normalize(str) {
return str.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase();
return str?.trim().toLowerCase() || '';
}
modal.addEventListener('show.bs.modal', async function () {
let addedProducts = new Set();
document.querySelectorAll('#items li').forEach(li => {
if (li.dataset.name) {
addedProducts.add(normalize(li.dataset.name));
let sortMode = 'popularity';
let limit = 25;
let offset = 0;
let loading = false;
let reachedEnd = false;
let allProducts = [];
let addedProducts = new Set();
function renderSortBar() {
if (!sortBar) return;
sortBar.innerHTML = `
Sortuj: <a href="#" id="sort-popularity" ${sortMode === "popularity" ? 'style="font-weight:bold"' : ''}>Popularność</a> |
<a href="#" id="sort-alphabetical" ${sortMode === "alphabetical" ? 'style="font-weight:bold"' : ''}>Alfabetycznie</a>
`;
document.getElementById('sort-popularity').onclick = (e) => {
e.preventDefault();
if (sortMode !== 'popularity') {
sortMode = 'popularity';
resetAndFetchProducts();
}
});
};
document.getElementById('sort-alphabetical').onclick = (e) => {
e.preventDefault();
if (sortMode !== 'alphabetical') {
sortMode = 'alphabetical';
resetAndFetchProducts();
}
};
}
function resetAndFetchProducts() {
offset = 0;
reachedEnd = false;
allProducts = [];
productList.innerHTML = '';
fetchProducts(true);
renderSortBar();
if (productCountDisplay) productCountDisplay.textContent = '';
}
async function fetchProducts(reset = false) {
if (loading || reachedEnd) return;
loading = true;
if (!reset) {
const loadingLi = document.createElement('li');
loadingLi.className = 'list-group-item bg-dark text-light loading';
loadingLi.textContent = 'Ładowanie...';
productList.appendChild(loadingLi);
}
productList.innerHTML = '<li class="list-group-item bg-dark text-light">Ładowanie...</li>';
try {
const res = await fetch('/all_products');
const res = await fetch(`/all_products?sort=${sortMode}&limit=${limit}&offset=${offset}`);
const data = await res.json();
const allproducts = data.allproducts;
productList.innerHTML = '';
allproducts.forEach(name => {
const li = document.createElement('li');
li.className = 'list-group-item d-flex justify-content-between align-items-center bg-dark text-light';
const products = data.products || [];
if (addedProducts.has(normalize(name))) {
const nameSpan = document.createElement('span');
nameSpan.textContent = name;
li.appendChild(nameSpan);
if (products.length < limit) reachedEnd = true;
allProducts = reset ? products : allProducts.concat(products);
li.classList.add('opacity-50');
const badge = document.createElement('span');
badge.className = 'badge bg-success ms-auto';
badge.textContent = 'Dodano';
li.appendChild(badge);
} else {
const nameSpan = document.createElement('span');
nameSpan.textContent = name;
nameSpan.style.flex = '1 1 auto';
li.appendChild(nameSpan);
const loadingEl = productList.querySelector('.loading');
if (loadingEl) loadingEl.remove();
const qtyWrapper = document.createElement('div');
qtyWrapper.className = 'd-flex align-items-center ms-2 quantity-controls';
if (reset && products.length === 0) {
const emptyLi = document.createElement('li');
emptyLi.className = 'list-group-item text-muted bg-dark';
emptyLi.textContent = 'Brak produktów do wyświetlenia.';
productList.appendChild(emptyLi);
} else {
renderProducts(products);
}
const minusBtn = document.createElement('button');
minusBtn.type = 'button';
minusBtn.className = 'btn btn-outline-light btn-sm px-2';
minusBtn.textContent = '';
minusBtn.onclick = () => {
qty.value = Math.max(1, parseInt(qty.value) - 1);
};
offset += limit;
const qty = document.createElement('input');
qty.type = 'number';
qty.min = 1;
qty.value = 1;
qty.className = 'form-control text-center p-1 rounded';
qty.style.width = '50px';
qty.style.margin = '0 2px';
qty.title = 'Ilość';
if (productCountDisplay) {
productCountDisplay.textContent = `Wyświetlono ${allProducts.length} z ${data.total_count} pozycji`;
}
const plusBtn = document.createElement('button');
plusBtn.type = 'button';
plusBtn.className = 'btn btn-outline-light btn-sm px-2';
plusBtn.textContent = '+';
plusBtn.onclick = () => {
qty.value = parseInt(qty.value) + 1;
};
const statsEl = document.getElementById('massAddProductStats');
if (statsEl) {
statsEl.textContent = `(${allProducts.length} z ${data.total_count})`;
}
qtyWrapper.appendChild(minusBtn);
qtyWrapper.appendChild(qty);
qtyWrapper.appendChild(plusBtn);
const btn = document.createElement('button');
btn.className = 'btn btn-sm btn-primary ms-4';
btn.textContent = '+';
btn.onclick = () => {
const quantity = parseInt(qty.value) || 1;
socket.emit('add_item', { list_id: LIST_ID, name: name, quantity: quantity });
};
li.appendChild(qtyWrapper);
li.appendChild(btn);
}
productList.appendChild(li);
});
} catch (err) {
productList.innerHTML = '<li class="list-group-item text-danger bg-dark">Błąd ładowania danych</li>';
const loadingEl = productList.querySelector('.loading');
if (loadingEl) loadingEl.remove();
const errorLi = document.createElement('li');
errorLi.className = 'list-group-item text-danger bg-dark';
errorLi.textContent = 'Błąd ładowania danych';
productList.appendChild(errorLi);
}
loading = false;
}
function getAlreadyAddedProducts() {
const set = new Set();
document.querySelectorAll('#items li').forEach(li => {
if (li.dataset.name) {
set.add(normalize(li.dataset.name));
}
});
return set;
}
function renderProducts(products) {
addedProducts = getAlreadyAddedProducts();
const existingNames = new Set();
document.querySelectorAll('#mass-add-list li').forEach(li => {
const name = li.querySelector('span')?.textContent;
if (name) existingNames.add(normalize(name));
});
products.forEach(product => {
const name = typeof product === "object" ? product.name : product;
const normName = normalize(name);
if (existingNames.has(normName)) return;
existingNames.add(normName);
const li = document.createElement('li');
li.className = 'list-group-item d-flex justify-content-between align-items-center bg-dark text-light';
if (addedProducts.has(normName)) {
const nameSpan = document.createElement('span');
nameSpan.textContent = name;
li.appendChild(nameSpan);
li.classList.add('opacity-50');
const badge = document.createElement('span');
badge.className = 'badge bg-success ms-auto';
badge.textContent = 'Dodano';
li.appendChild(badge);
} else {
const nameSpan = document.createElement('span');
nameSpan.textContent = name;
nameSpan.style.flex = '1 1 auto';
li.appendChild(nameSpan);
const qtyWrapper = document.createElement('div');
qtyWrapper.className = 'd-flex align-items-center ms-2 quantity-controls';
const minusBtn = document.createElement('button');
minusBtn.type = 'button';
minusBtn.className = 'btn btn-outline-light btn-sm px-2';
minusBtn.textContent = '';
const qty = document.createElement('input');
qty.type = 'number';
qty.min = 1;
qty.value = 1;
qty.className = 'form-control text-center p-1 rounded';
qty.style.width = '50px';
qty.style.margin = '0 2px';
qty.title = 'Ilość';
const plusBtn = document.createElement('button');
plusBtn.type = 'button';
plusBtn.className = 'btn btn-outline-light btn-sm px-2';
plusBtn.textContent = '+';
minusBtn.onclick = () => {
qty.value = Math.max(1, parseInt(qty.value) - 1);
};
plusBtn.onclick = () => {
qty.value = parseInt(qty.value) + 1;
};
qtyWrapper.append(minusBtn, qty, plusBtn);
const btn = document.createElement('button');
btn.className = 'btn btn-sm btn-primary ms-4';
btn.textContent = '+';
btn.onclick = () => {
const quantity = parseInt(qty.value) || 1;
socket.emit('add_item', { list_id: LIST_ID, name: name, quantity: quantity });
};
li.append(qtyWrapper, btn);
}
productList.appendChild(li);
});
}
if (modalBody) {
modalBody.addEventListener('scroll', function () {
if (!loading && !reachedEnd && (modalBody.scrollTop + modalBody.clientHeight > modalBody.scrollHeight - 80)) {
fetchProducts(false);
}
});
}
modal.addEventListener('show.bs.modal', function () {
resetAndFetchProducts();
});
renderSortBar();
socket.on('item_added', data => {
document.querySelectorAll('#mass-add-list li').forEach(li => {
const itemName = li.firstChild?.textContent.trim();
if (normalize(itemName) === normalize(data.name) && !li.classList.contains('opacity-50')) {
li.classList.add('opacity-50');
// Usuń poprzednie przyciski
li.querySelectorAll('button').forEach(btn => btn.remove());
const quantityControls = li.querySelector('.quantity-controls');
if (quantityControls) quantityControls.remove();
// Badge "Dodano"
const badge = document.createElement('span');
badge.className = 'badge bg-success';
badge.textContent = 'Dodano';
// Grupowanie przycisku + licznika
const btnGroup = document.createElement('div');
btnGroup.className = 'btn-group btn-group-sm me-2';
btnGroup.role = 'group';
@@ -124,17 +233,13 @@ document.addEventListener('DOMContentLoaded', function () {
let secondsLeft = 15;
timerBtn.textContent = `${secondsLeft}s`;
btnGroup.appendChild(undoBtn);
btnGroup.appendChild(timerBtn);
btnGroup.append(undoBtn, timerBtn);
// Kontener na prawą stronę
const rightWrapper = document.createElement('div');
rightWrapper.className = 'd-flex align-items-center gap-2 ms-auto';
rightWrapper.appendChild(btnGroup);
rightWrapper.appendChild(badge);
rightWrapper.append(btnGroup, badge);
li.appendChild(rightWrapper);
// Odliczanie
const intervalId = setInterval(() => {
secondsLeft--;
if (secondsLeft > 0) {
@@ -145,14 +250,12 @@ document.addEventListener('DOMContentLoaded', function () {
}
}, 1000);
// Obsługa cofnięcia
undoBtn.onclick = () => {
clearInterval(intervalId);
btnGroup.remove();
badge.remove();
li.classList.remove('opacity-50');
// Przywróć kontrolki ilości
const qtyWrapper = document.createElement('div');
qtyWrapper.className = 'd-flex align-items-center ms-2 quantity-controls';
@@ -185,7 +288,6 @@ document.addEventListener('DOMContentLoaded', function () {
qtyWrapper.append(minusBtn, qty, plusBtn);
li.appendChild(qtyWrapper);
// Dodaj przycisk dodawania
const addBtn = document.createElement('button');
addBtn.className = 'btn btn-sm btn-primary ms-4';
addBtn.textContent = '+';
@@ -199,13 +301,9 @@ document.addEventListener('DOMContentLoaded', function () {
};
li.appendChild(addBtn);
// Usuń z listy
socket.emit('delete_item', { item_id: data.id });
};
}
});
});
});

View File

@@ -66,7 +66,7 @@ function bindDeleteButton(button) {
const syncBtn = cell.querySelector('.sync-btn');
if (syncBtn) bindSyncButton(syncBtn);
} else {
cell.innerHTML = '<span class="badge rounded-pill bg-warning opacity-75">Usunięto synchronizacje</span>';
cell.innerHTML = '<span class="badge rounded-pill bg-warning opacity-75">Usunięto z bazy danych</span>';
}
})
.catch(() => {

View File

@@ -21,8 +21,8 @@ async function analyzeReceipts(listId) {
const duration = ((performance.now() - start) / 1000).toFixed(2);
let html = `<div class="card bg-dark text-white border-secondary p-3">`;
html += `<p><b>📊 Łącznie wykryto:</b> ${data.total.toFixed(2)} PLN</p>`;
html += `<p class="text-secondary"><small>⏱ Czas analizy OCR: ${duration} sek.</small></p>`;
html += `<p><b>📊 Łącznie wykryto:</b> ${data.total.toFixed(2)} PLN</p>`;
data.results.forEach((r, i) => {
const disabled = r.already_added ? "disabled" : "";
@@ -30,8 +30,8 @@ async function analyzeReceipts(listId) {
const inputField = `<input type="number" id="amount-${i}" value="${r.amount}" step="0.01" class="${inputStyle}" style="width: 120px;" ${disabled}>`;
const button = r.already_added
? `<span class="badge bg-primary ms-2">Dodano</span>`
: `<button id="add-btn-${i}" onclick="emitExpense(${i})" class="btn btn-sm btn-outline-success ms-2"> Dodaj</button>`;
? `<span class="badge rounded-pill bg-secondary ms-2">Dodano</span>`
: `<button id="add-btn-${i}" onclick="emitExpense(${i})" class="btn btn-outline-light ms-2"> Dodaj</button>`;
html += `
<div class="mb-2 d-flex align-items-center gap-2 flex-wrap">
@@ -43,7 +43,7 @@ async function analyzeReceipts(listId) {
if (data.results.length > 1) {
html += `<button id="addAllBtn" onclick="emitAllExpenses(${data.results.length})" class="btn btn-success mt-3 w-100"> Dodaj wszystkie</button>`;
html += `<button id="addAllBtn" onclick="emitAllExpenses(${data.results.length})" class="btn btn-sm btn-outline-light mt-3 w-100"> Dodaj wszystkie</button>`;
}
html += `</div>`;

View File

@@ -0,0 +1,17 @@
document.addEventListener('DOMContentLoaded', function () {
const showAllCheckbox = document.getElementById('showAllLists');
if (!showAllCheckbox) return;
const params = new URLSearchParams(window.location.search);
if (!params.has('show_all')) {
params.set('show_all', 'true');
window.history.replaceState({}, '', `${window.location.pathname}?${params.toString()}`);
}
showAllCheckbox.checked = params.get('show_all') === 'true';
showAllCheckbox.addEventListener('change', function () {
const urlParams = new URLSearchParams(window.location.search);
urlParams.set('show_all', showAllCheckbox.checked ? 'true' : 'false');
window.location.search = urlParams.toString();
});
});

28
static/js/table_search.js Normal file
View File

@@ -0,0 +1,28 @@
document.addEventListener("DOMContentLoaded", function () {
const searchInput = document.getElementById("search-table");
const clearButton = document.getElementById("clear-search");
const rows = document.querySelectorAll("table tbody tr");
if (!searchInput || !rows.length) return;
function filterTable(query) {
const q = query.toLowerCase();
rows.forEach(row => {
const rowText = row.textContent.toLowerCase();
row.style.display = rowText.includes(q) ? "" : "none";
});
}
searchInput.addEventListener("input", function () {
filterTable(this.value);
});
if (clearButton) {
clearButton.addEventListener("click", function () {
searchInput.value = "";
filterTable(""); // Pokaż wszystko
searchInput.focus();
});
}
});

View File

@@ -4,10 +4,10 @@
<div class="d-flex justify-content-between align-items-center flex-wrap mb-4">
<h2 class="mb-2">⚙️ Panel administratora</h2>
<a href="/" class="btn btn-outline-secondary">← Powrót do strony głównej</a>
<a href="{{ url_for('main_page') }}" class="btn btn-outline-secondary">← Powrót do strony głównej</a>
</div>
<div class="card bg-dark text-white mb-4">
<div class="card bg-secondary bg-opacity-10 text-white mb-4">
<div class="card-body p-2">
<div class="d-flex flex-wrap gap-2">
<a href="{{ url_for('list_users') }}" class="btn btn-outline-light btn-sm">👥 Użytkownicy</a>
@@ -35,19 +35,43 @@
<td class="text-end fw-bold">{{ list_count }}</td>
</tr>
<tr>
<td>🛒 Produkty</td>
<td>🛒 Produkty na listach</td>
<td class="text-end fw-bold">{{ item_count }}</td>
</tr>
<tr>
<td>✅ Zakupione</td>
<td class="text-end fw-bold">{{ purchased_items_count }}</td>
</tr>
<tr>
<td>🚫 Nieoznaczone jako kupione</td>
<td class="text-end fw-bold">{{ not_purchased_count }}</td>
</tr>
<tr>
<td>✍️ Produkty z notatkami</td>
<td class="text-end fw-bold">{{ items_with_notes }}</td>
</tr>
<tr>
<td>🕓 Śr. czas do zakupu (h)</td>
<td class="text-end fw-bold">{{ avg_hours_to_purchase }}</td>
</tr>
<tr>
<td>💸 Średnia kwota na listę</td>
<td class="text-end fw-bold">{{ avg_list_expense }} zł</td>
</tr>
</tbody>
</table>
<hr>
<div class="small text-uppercase mb-1">📈 Średnie tempo tworzenia list:</div>
<ul class="list-unstyled small mb-0">
<li>📆 Tygodniowo: <strong>{{ avg_per_week }}</strong></li>
<li>🗓️ Miesięcznie: <strong>{{ avg_per_month }}</strong></li>
<!--< li>📅 Rocznie: <strong>{{ avg_per_year }}</strong></li> -->
</ul>
</div>
</div>
</div>
<!-- Najczęściej kupowane -->
<div class="col-md-4">
<div class="card bg-dark text-white h-100">
@@ -59,7 +83,7 @@
<div class="mb-2">
<div class="d-flex justify-content-between">
<span>{{ name }}</span>
<span class="badge rounded-pill bg-secondary opacity-75">{{ count }}×</span>
<span class="badge rounded-pill bg-secondary">{{ count }}x</span>
</div>
<div class="progress bg-transparent" style=" height: 6px;">
<div class="progress-bar bg-success" role="progressbar" style="width: {{ (count / max_count) * 100 }}%"
@@ -69,25 +93,28 @@
</div>
{% endfor %}
{% else %}
<span class="badge rounded-pill bg-secondary opacity-75">Brak danych</p>
{% endif %}
<div>
<p><span class="badge rounded-pill bg-secondary opacity-75">Brak danych</span></p>
</div>
{% endif %}
</div>
</div>
</div>
<!-- Podsumowanie wydatków -->
<div class="col-md-4">
<div class="card bg-dark text-white h-100">
<div class="card bg-dark text-white h-100 shadow-sm">
<div class="card-body">
<h5>💸 Podsumowanie wydatków:</h5>
<h5 class="mb-3">💸 Podsumowanie wydatków</h5>
<table class="table table-dark table-sm mb-3">
<thead>
<table class="table table-dark table-sm mb-3 align-middle">
<thead class="text-muted small">
<tr>
<th>Typ listy</th>
<th>Miesiąc</th>
<th>Rok</th>
<th>Całkowite</th>
<th title="Rodzaj listy zakupowej">Typ listy</th>
<th title="Wydatki w bieżącym miesiącu">Miesiąc</th>
<th title="Wydatki w bieżącym roku">Rok</th>
<th title="Wydatki łączne">Całkowite</th>
<!-- <th title="Średnia kwota na 1 listę">Średnia</th> -->
</tr>
</thead>
<tbody>
@@ -96,225 +123,238 @@
<td>{{ '%.2f'|format(expense_summary.all.month) }} PLN</td>
<td>{{ '%.2f'|format(expense_summary.all.year) }} PLN</td>
<td>{{ '%.2f'|format(expense_summary.all.total) }} PLN</td>
<!-- <td>{{ '%.2f'|format(expense_summary.all.avg) }} PLN</td> -->
</tr>
<tr>
<td>Aktywne</td>
<td>{{ '%.2f'|format(expense_summary.active.month) }} PLN</td>
<td>{{ '%.2f'|format(expense_summary.active.year) }} PLN</td>
<td>{{ '%.2f'|format(expense_summary.active.total) }} PLN</td>
<!-- <td>{{ '%.2f'|format(expense_summary.active.avg) }} PLN</td> -->
</tr>
<tr>
<td>Archiwalne</td>
<td>{{ '%.2f'|format(expense_summary.archived.month) }} PLN</td>
<td>{{ '%.2f'|format(expense_summary.archived.year) }} PLN</td>
<td>{{ '%.2f'|format(expense_summary.archived.total) }} PLN</td>
<!-- <td>{{ '%.2f'|format(expense_summary.archived.avg) }} PLN</td> -->
</tr>
<tr>
<td>Wygasłe</td>
<td>{{ '%.2f'|format(expense_summary.expired.month) }} PLN</td>
<td>{{ '%.2f'|format(expense_summary.expired.year) }} PLN</td>
<td>{{ '%.2f'|format(expense_summary.expired.total) }} PLN</td>
<!-- <td>{{ '%.2f'|format(expense_summary.expired.avg) }} PLN</td> -->
</tr>
</tbody>
</table>
<a href="{{ url_for('expenses') }}#chartTab" class="btn btn-outline-info">
<a href="{{ url_for('expenses') }}#chartTab" class="btn btn-outline-light w-100">
📊 Pokaż wykres wydatków
</a>
</div>
</div>
</div>
</div>
{# panel wyboru miesiąca zawsze widoczny #}
<div class="d-flex justify-content-between align-items-center mb-3 flex-wrap gap-2">
<div class="card bg-dark text-white mb-5">
<div class="card-body">
{# LEWA STRONA — przyciski ← → TYLKO gdy nie show_all #}
<div class="d-flex gap-2">
{% if not show_all %}
{% set current_date = now.replace(day=1) %}
{% set prev_month = (current_date - timedelta(days=1)).strftime('%Y-%m') %}
{% set next_month = (current_date + timedelta(days=31)).replace(day=1).strftime('%Y-%m') %}
{# panel wyboru miesiąca zawsze widoczny #}
<div class="d-flex justify-content-between align-items-center mb-3 flex-wrap gap-2">
{% if prev_month in month_options %}
<a href="{{ url_for('admin_panel', m=prev_month) }}" class="btn btn-outline-light btn-sm">
← {{ prev_month }}
</a>
{% else %}
<button class="btn btn-outline-light btn-sm opacity-50" disabled>← {{ prev_month }}</button>
{% endif %}
{# LEWA STRONA — przyciski ← → TYLKO gdy nie show_all #}
<div class="d-flex gap-2">
{% if not show_all %}
{% set current_date = now.replace(day=1) %}
{% set prev_month = (current_date - timedelta(days=1)).strftime('%Y-%m') %}
{% set next_month = (current_date + timedelta(days=31)).replace(day=1).strftime('%Y-%m') %}
{% if next_month in month_options %}
<a href="{{ url_for('admin_panel', m=next_month) }}" class="btn btn-outline-light btn-sm">
{{ next_month }}
</a>
{% else %}
<button class="btn btn-outline-light btn-sm opacity-50" disabled>{{ next_month }}</button>
{% endif %}
{% else %}
{# Tryb wszystkie miesiące — możemy pokazać skrót do bieżącego miesiąca #}
<a href="{{ url_for('admin_panel', m=now.strftime('%Y-%m')) }}" class="btn btn-outline-light btn-sm">
📅 Przejdź do bieżącego miesiąca
</a>
{% endif %}
</div>
{% if prev_month in month_options %}
<a href="{{ url_for('admin_panel', m=prev_month) }}" class="btn btn-outline-light btn-sm">
← {{ prev_month }}
</a>
{% else %}
<button class="btn btn-outline-light btn-sm opacity-50" disabled>{{ prev_month }}</button>
{% endif %}
{# PRAWA STRONA — picker miesięcy zawsze widoczny #}
<form method="get" class="m-0">
<div class="input-group input-group-sm">
<span class="input-group-text bg-secondary text-white">📅</span>
<select name="m" class="form-select bg-dark text-white border-secondary" onchange="this.form.submit()">
<option value="all" {% if show_all %}selected{% endif %}>Wszystkie miesiące</option>
{% for val in month_options %}
{% set date_obj = (val ~ '-01') | todatetime %}
<option value="{{ val }}" {% if month_str==val %}selected{% endif %}>
{{ date_obj.strftime('%B %Y')|capitalize }}
</option>
{% endfor %}
</select>
</div>
</form>
</div>
{% if next_month in month_options %}
<a href="{{ url_for('admin_panel', m=next_month) }}" class="btn btn-outline-light btn-sm">
{{ next_month }} →
</a>
{% else %}
<button class="btn btn-outline-light btn-sm opacity-50" disabled>{{ next_month }} →</button>
{% endif %}
{% else %}
{# Tryb wszystkie miesiące — możemy pokazać skrót do bieżącego miesiąca #}
<a href="{{ url_for('admin_panel', m=now.strftime('%Y-%m')) }}" class="btn btn-outline-light btn-sm">
📅 Przejdź do bieżącego miesiąca
</a>
{% endif %}
</div>
<div class="card bg-dark text-white mb-5">
<div class="card-body">
<h3 class="mt-4">
📄 Listy zakupowe
{% if show_all %}
<strong>wszystkie miesiące</strong>
{% else %}
<strong>{{ month_str|replace('-', ' / ') }}</strong>
{% endif %}
</h3>
<form method="post" action="{{ url_for('delete_selected_lists') }}">
<div class="table-responsive">
<table class="table table-dark table-striped align-middle sortable">
<thead>
<tr>
<th><input type="checkbox" id="select-all"></th>
<th>ID</th>
<th>Tytuł</th>
<th>Status</th>
<th>Utworzono</th>
<th>Właściciel</th>
<th>Produkty</th>
<th>Progress</th>
<th>Koment.</th>
<th>Paragony</th>
<th>Wydatki</th>
<th>Akcje</th>
</tr>
</thead>
<tbody>
{% for e in enriched_lists %}
{% set l = e.list %}
<tr>
<td><input type="checkbox" name="list_ids" value="{{ l.id }}"></td>
<td>{{ l.id }}</td>
<td class="fw-bold align-middle">
<a href="{{ url_for('view_list', list_id=l.id) }}" class="text-white">{{ l.title }}</a>
{% if l.categories %}
<span class="ms-1 text-info" data-bs-toggle="tooltip"
title="{{ l.categories | map(attribute='name') | join(', ') }}">
🏷
</span>
{% endif %}
</td>
{# PRAWA STRONA — picker miesięcy zawsze widoczny #}
<form method="get" class="m-0">
<div class="input-group input-group-sm">
<span class="input-group-text bg-secondary text-white">📅</span>
<select name="m" class="form-select bg-dark text-white border-secondary" onchange="this.form.submit()">
<option value="all" {% if show_all %}selected{% endif %}>Wszystkie miesiące</option>
{% for val in month_options %}
{% set date_obj = (val ~ '-01') | todatetime %}
<option value="{{ val }}" {% if month_str==val %}selected{% endif %}>
{{ date_obj.strftime('%B %Y')|capitalize }}
</option>
{% endfor %}
</select>
</div>
</form>
</div>
<td>
{% if l.is_archived %}
<span class="badge rounded-pill bg-secondary">Archiwalna</span>
{% elif e.expired %}
<span class="badge rounded-pill bg-warning text-dark">Wygasła</span>
{% else %}
<span class="badge rounded-pill bg-success">Aktywna</span>
{% endif %}
</td>
<td>{{ l.created_at.strftime('%Y-%m-%d %H:%M') if l.created_at else '-' }}</td>
<td>
{% if l.owner %}
👤 {{ l.owner.username }} ({{ l.owner.id }})
{% else %}
-
{% endif %}
</td>
<td>{{ e.total_count }}</td>
<td>
<div class="progress bg-transparent" style=" height: 14px;">
<div class="progress-bar fw-bold text-black text-cente
<h3 class="mt-4">
📄 Listy zakupowe
{% if show_all %}
<strong>wszystkie miesiące</strong>
{% else %}
<strong>{{ month_str|replace('-', ' / ') }}</strong>
{% endif %}
</h3>
<form method="post" action="{{ url_for('admin_delete_list') }}">
<div class="table-responsive">
<table class="table table-dark align-middle sortable">
<thead>
<tr>
<th><input type="checkbox" id="select-all"></th>
<th>ID</th>
<th>Tytuł</th>
<th>Status</th>
<th>Utworzono</th>
<th>Właściciel</th>
<th>Produkty</th>
<th>Progress</th>
<th>Koment.</th>
<th>Paragony</th>
<th>Wydatki</th>
<th>Akcje</th>
</tr>
</thead>
<tbody>
{% for e in enriched_lists %}
{% set l = e.list %}
<tr>
<td><input type="checkbox" name="list_ids" value="{{ l.id }}"></td>
<td>{{ l.id }}</td>
<td class="fw-bold align-middle">
<a href="{{ url_for('view_list', list_id=l.id) }}" class="text-white">{{ l.title }}</a>
{% if l.categories %}
<span class="ms-1 text-info" data-bs-toggle="tooltip"
title="{{ l.categories | map(attribute='name') | join(', ') }}">
🏷
</span>
{% endif %}
</td>
<td>
{% if l.is_archived %}
<span class="badge rounded-pill bg-secondary">Archiwalna</span>
{% elif e.expired %}
<span class="badge rounded-pill bg-warning text-dark">Wygasła</span>
{% else %}
<span class="badge rounded-pill bg-success">Aktywna</span>
{% endif %}
</td>
<td>{{ l.created_at.strftime('%Y-%m-%d %H:%M') if l.created_at else '-' }}</td>
<td>
{% if l.owner %}
👤 {{ l.owner.username }} ({{ l.owner.id }})
{% else %}
-
{% endif %}
</td>
<td>{{ e.total_count }}</td>
<td>
<div class="progress bg-transparent" style=" height: 14px;">
<div class="progress-bar fw-bold text-black text-cente
{% if e.percent >= 80 %}bg-success
{% elif e.percent >= 40 %}bg-warning
{% else %}bg-danger{% endif %}" role="progressbar" style="width: {{ e.percent }}%">
{{ e.purchased_count }}/{{ e.total_count }}
{{ e.purchased_count }}/{{ e.total_count }}
</div>
</div>
</div>
</td>
<td><span class="badge rounded-pill bg-primary">{{ e.comments_count }}</span></td>
<td><span class="badge rounded-pill bg-secondary">{{ e.receipts_count }}</span></td>
<td class="fw-bold
</td>
<td><span class="badge rounded-pill bg-primary">{{ e.comments_count }}</span></td>
<td><span class="badge rounded-pill bg-secondary">{{ e.receipts_count }}</span></td>
<td class="fw-bold
{% if e.total_expense >= 500 %}text-danger
{% elif e.total_expense > 0 %}text-success{% endif %}">
{% if e.total_expense > 0 %}
{{ '%.2f'|format(e.total_expense) }} PLN
{% else %}
-
{% endif %}
</td>
<td>
<div class="btn-group btn-group-sm" role="group">
<a href="{{ url_for('edit_list', list_id=l.id) }}" class="btn btn-sm btn-outline-light"
title="Edytuj">✏️</a>
<button type="button" class="btn btn-sm btn-outline-light preview-btn" data-list-id="{{ l.id }}"
title="Podgląd produktów">
👁️
</button>
<a href="{{ url_for('delete_list', list_id=l.id) }}" class="btn btn-sm btn-outline-light"
onclick="return confirm('Na pewno usunąć tę listę?')" title="Usuń">🗑️</a>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="d-flex justify-content-end mt-2">
<button type="submit" class="btn btn-danger">🗑️ Usuń zaznaczone listy</button>
</div>
</form>
{% if e.total_expense > 0 %}
{{ '%.2f'|format(e.total_expense) }} PLN
{% else %}
-
{% endif %}
</td>
<td>
<div class="btn-group btn-group-sm" role="group">
<a href="{{ url_for('edit_list', list_id=l.id) }}" class="btn btn-sm btn-outline-light"
title="Edytuj">✏️</a>
<button type="button" class="btn btn-sm btn-outline-light preview-btn" data-list-id="{{ l.id }}"
title="Podgląd produktów">
👁️
</button>
<form method="post" action="{{ url_for('admin_delete_list') }}"
onsubmit="return confirm('Na pewno usunąć tę listę?')" class="d-inline">
<input type="hidden" name="single_list_id" value="{{ l.id }}">
<button type="submit" class="btn btn-sm btn-outline-light" title="Usuń">🗑️</button>
</form>
</div>
</td>
</tr>
{% endfor %}
{% if enriched_lists|length == 0 %}
<tr>
<td colspan="12" class="text-center py-4">
Brak list zakupowych do wyświetlenia
</td>
</tr>
{% endif %}
</tbody>
</table>
</div>
<div class="d-flex justify-content-end mt-2">
<button type="submit" class="btn btn-outline-light btn-sm">🗑️ Usuń zaznaczone listy</button>
</div>
</form>
</div>
</div>
</div>
<div class="info-bar-fixed">
Python: {{ python_version.split()[0] }} | {{ system_info }} | RAM app: {{ app_memory }} |
DB: {{ db_info.engine|upper }}{% if db_info.version %} v{{ db_info.version[0] }}{% endif %} |
Tabele: {{ table_count }} | Rekordy: {{ record_total }} |
Uptime: {{ uptime_minutes }} min
</div>
<div class="info-bar-fixed">
Python: {{ python_version.split()[0] }} | {{ system_info }} | RAM app: {{ app_memory }} |
DB: {{ db_info.engine|upper }}{% if db_info.version %} v{{ db_info.version[0] }}{% endif %} |
Tabele: {{ table_count }} | Rekordy: {{ record_total }} |
Uptime: {{ uptime_minutes }} min
</div>
<!-- Modal podglądu produktów -->
<div class="modal fade" id="productPreviewModal" tabindex="-1" aria-labelledby="previewModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg modal-dialog-scrollable">
<div class="modal-content bg-dark text-white">
<div class="modal-header">
<h5 class="modal-title" id="previewModalLabel">Podgląd produktów</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Zamknij"></button>
</div>
<div class="modal-body">
<ul id="product-list" class="list-group list-group-flush"></ul>
<!-- Modal podglądu produktów -->
<div class="modal fade" id="productPreviewModal" tabindex="-1" aria-labelledby="previewModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg modal-dialog-scrollable">
<div class="modal-content bg-dark text-white">
<div class="modal-header">
<h5 class="modal-title" id="previewModalLabel">Podgląd produktów</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Zamknij"></button>
</div>
<div class="modal-body">
<ul id="product-list" class="list-group list-group-flush"></ul>
</div>
</div>
</div>
</div>
</div>
{% block scripts %}
<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='preview_list_modal.js') }}"></script>
{% endblock %}
{% block scripts %}
<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='preview_list_modal.js') }}"></script>
{% endblock %}
{% endblock %}
{% endblock %}

View File

@@ -7,7 +7,7 @@
<a href="{{ url_for('admin_panel') }}" class="btn btn-outline-secondary">← Powrót do panelu</a>
</div>
<div class="card bg-dark text-white mb-5">
<div class="card bg-secondary bg-opacity-10 text-white mb-5">
<div class="card-body">
<h4 class="card-title">📄 Podstawowe informacje</h4>
<form method="post" class="mt-3">
@@ -83,9 +83,11 @@
<div class="row mb-4">
<div class="col-md-6">
<label class="form-label">📆 Utworzono</label>
<p class="form-control-plaintext text-white">
{{ list.created_at.strftime('%Y-%m-%d') }}
</p>
<div>
<span class="badge rounded-pill bg-success rounded-pill text-dark ms-1">
{{ list.created_at.strftime('%Y-%m-%d') }}
</span>
</div>
</div>
<div class="col-md-6">
<label class="form-label">📁 Przenieś do miesiąca (format: rok-miesiąc np 2026-01)</label>
@@ -115,7 +117,7 @@
value="{{ request.url_root }}share/{{ list.share_token }}">
</div>
<button type="submit" class="btn btn-success me-2">💾 Zapisz zmiany</button>
<button type="submit" class="btn btn-outline-light btn-sm me-2">💾 Zapisz zmiany</button>
</form>
</div>
@@ -136,7 +138,7 @@
value="1">
</div>
<div class="col-md-3 d-grid">
<button type="submit" class="btn btn-outline-success"> Dodaj</button>
<button type="submit" class="btn btn-outline-light"> Dodaj</button>
</div>
</form>
@@ -173,7 +175,7 @@
<div class="input-group input-group-sm w-auto">
<input type="number" name="quantity" class="form-control bg-dark text-white border-secondary" min="1"
value="{{ item.quantity }}">
<button type="submit" class="btn btn-outline-light">💾</button>
<button type="submit" class="btn btn-outline-light btn-sm">💾</button>
</div>
</form>
</td>
@@ -191,14 +193,15 @@
<input type="hidden" name="item_id" value="{{ item.id }}">
<div class="btn-group btn-group-sm d-flex gap-1">
{% if not item.not_purchased %}
<button type="submit" name="action" value="toggle_purchased" class="btn btn-outline-light w-100">
<button type="submit" name="action" value="toggle_purchased" class="btn btn-outline-light btn-sm">
{{ '🚫 Odznacz' if item.purchased else '✅ Kupiony' }}
</button>
<button type="submit" name="action" value="mark_not_purchased" class="btn btn-outline-light w-100">⚠️
<button type="submit" name="action" value="mark_not_purchased" class="btn btn-outline-light btn-sm">⚠️
Nie kupiony</button>
{% endif %}
{% if item.not_purchased %}
<button type="submit" name="action" value="unmark_not_purchased" class="btn btn-outline-light w-100">
<button type="submit" name="action" value="unmark_not_purchased"
class="btn btn-outline-light btn-sm">
Przywróć jako nieoznaczone</button>
{% endif %}
</div>
@@ -208,7 +211,7 @@
<form method="post" action="{{ url_for('edit_list', list_id=list.id) }}">
<input type="hidden" name="action" value="delete_item">
<input type="hidden" name="item_id" value="{{ item.id }}">
<button type="submit" class="btn btn-outline-light btn-sm w-100">🗑️</button>
<button type="submit" class="btn btn-outline-light btn-sm">🗑️</button>
</form>
</td>
</tr>
@@ -229,41 +232,34 @@
<div class="mb-3 text-end">
<a href="{{ url_for('admin_receipts', id=list.id) }}" class="btn btn-sm btn-outline-light">
📂 Otwórz widok pełny dla tej listy
📂 Otwórz zarządzanie paragonami
</a>
</div>
<div class="row g-3">
{% for r in receipts %}
<div class="col-6 col-md-4 col-lg-3">
<div class="card bg-dark text-white h-100">
<a href="{{ url_for('uploaded_file', filename=r.filename) }}" class="glightbox" data-gallery="receipts"
data-title="{{ r.filename }}">
<img src="{{ url_for('uploaded_file', filename=r.filename) }}" class="card-img-top"
style="object-fit: cover; height: 200px;">
<div class="card bg-dark text-white h-100 shadow-sm border border-secondary">
<a href="{{ url_for('uploaded_file', filename=r.filename) }}?v={{ r.version_token or '0' }}" class="glightbox"
data-gallery="receipts" data-title="{{ r.filename }}">
<img src="{{ url_for('uploaded_file', filename=r.filename) }}?v={{ r.version_token or '0' }}"
class="card-img-top" style="object-fit: cover; height: 200px;" title="{{ r.filename }}">
</a>
<div class="card-body text-center">
<p class="small text-truncate mb-1">{{ r.filename }}</p>
<p class="small mb-1">Wgrano: {{ r.uploaded_at.strftime('%Y-%m-%d %H:%M') }}</p>
{% if r.filesize and r.filesize >= 1024 * 1024 %}
<p class="small mb-1">Rozmiar: {{ (r.filesize / 1024 / 1024) | round(2) }} MB</p>
{% elif r.filesize %}
<p class="small mb-1">Rozmiar: {{ (r.filesize / 1024) | round(1) }} kB</p>
{% else %}
<p class="small mb-1 text-muted">Brak danych o rozmiarze</p>
{% endif %}
<a href="{{ url_for('rotate_receipt', receipt_id=r.id) }}"
class="btn btn-sm btn-outline-warning w-100 mb-2">🔄 Obróć o 90°</a>
<a href="{{ url_for('rename_receipt', receipt_id=r.id) }}" class="btn btn-sm btn-outline-info w-100 mb-2">✏️
Zmień nazwę</a>
{% if not r.file_hash %}
<a href="{{ url_for('generate_receipt_hash', receipt_id=r.id) }}"
class="btn btn-sm btn-outline-secondary w-100 mb-2">🔐 Generuj hash</a>
{% endif %}
<a href="{{ url_for('delete_receipt', receipt_id=r.id) }}"
class="btn btn-sm btn-outline-danger w-100 mb-2">🗑️ Usuń</a>
<div class="card-body text-center p-2 small">
<div class="text-truncate fw-semibold" title="{{ r.filename }}">📄 {{ r.filename }}</div>
<div>📅 {{ r.uploaded_at.strftime('%Y-%m-%d %H:%M') }}</div>
<div>👤 {{ r.uploaded_by_user.username if r.uploaded_by_user else "?" }}</div>
<div>
💾
{% if r.filesize and r.filesize >= 1024 * 1024 %}
{{ (r.filesize / 1024 / 1024) | round(2) }} MB
{% elif r.filesize %}
{{ (r.filesize / 1024) | round(1) }} kB
{% else %}
Brak danych
{% endif %}
</div>
</div>
</div>
</div>
@@ -272,13 +268,12 @@
{% if not receipts %}
<div class="alert alert-info text-center mt-3" role="alert">
Brak paragonów.
Brak paragonów
</div>
{% endif %}
</div>
</div>
{% endblock %}
{% block scripts %}
<script src="{{ url_for('static_bp.serve_js', filename='select.js') }}"></script>

View File

@@ -7,6 +7,32 @@
<a href="{{ url_for('admin_panel') }}" class="btn btn-outline-secondary">← Powrót do panelu</a>
</div>
<div class="card bg-secondary bg-opacity-10 text-white mb-4">
<div class="card-body">
<!-- Formularz dodawania sugestii -->
<form action="{{ url_for('add_suggestion') }}" method="POST" class="mb-4">
<label class="form-label fw-bold mb-2"> Dodaj nową sugestię:</label>
<div class="input-group">
<input type="text" name="suggestion_name" class="form-control bg-dark text-white border-secondary"
placeholder="Nowa sugestia produktu…" required>
<button type="submit" class="btn btn-outline-light"> Dodaj</button>
</div>
</form>
<hr class="border-secondary opacity-50 mb-4 mt-2">
<!-- Szukajka z przyciskiem wyczyść -->
<label for="search-table" class="form-label fw-bold mb-2">🔍 Przeszukaj tabelę produktów i sugestii:</label>
<div class="input-group">
<input type="text" id="search-table" class="form-control bg-dark text-white border-secondary"
placeholder="Wpisz frazę, np. 'mleko'">
<button type="button" id="clear-search" class="btn btn-outline-light">🧹 Wyczyść</button>
</div>
</div>
</div>
<div class="card bg-dark text-white mb-5">
<div class="card-body">
<div class="card-header d-flex justify-content-between align-items-center">
@@ -14,7 +40,7 @@
<span class="badge rounded-pill bg-info">{{ total_items }} produktów</span>
</div>
<div class="card-body p-0">
<table class="table table-dark table-striped align-middle sortable">
<table class="table table-dark align-middle sortable">
<thead>
<tr>
<th>ID</th>
@@ -53,9 +79,12 @@
{% endfor %}
{% if items|length == 0 %}
<tr>
<td colspan="5" class="text-center">Pusta lista produktów.</td>
<td colspan="12" class="text-center py-4">
Pusta lista produktów
</td>
</tr>
{% endif %}
</tbody>
</table>
</div>
@@ -70,7 +99,7 @@
</div>
<div class="card-body p-0">
{% set item_names = items | map(attribute='name') | map('lower') | list %}
<table class="table table-dark table-striped align-middle sortable">
<table class="table table-dark align-middle sortable">
<thead>
<tr>
<th>ID</th>
@@ -93,7 +122,9 @@
{% endfor %}
{% if orphan_suggestions|length == 0 %}
<tr>
<td colspan="3" class="text-center">Brak sugestii do wyświetlenia.</td>
<td colspan="12" class="text-center py-4">
Brak niepowiązanych sugestii do wyświetlenia
</td>
</tr>
{% endif %}
</tbody>
@@ -101,14 +132,19 @@
</div>
</div>
</div>
<hr>
<div class="d-flex justify-content-between align-items-center mt-4">
<form method="get" class="d-flex align-items-center">
<label for="per_page" class="me-2">🔢 Pozycji na stronę:</label>
<select id="per_page" name="per_page" class="form-select form-select-sm me-2" onchange="this.form.submit()">
<option value="100" {% if per_page==100 %}selected{% endif %}>100</option>
<option value="200" {% if per_page==200 %}selected{% endif %}>200</option>
<option value="300" {% if per_page==300 %}selected{% endif %}>300</option>
<select id="per_page" name="per_page" class="form-select form-select-sm me-2"
onchange="this.form.page.value = 1; this.form.submit();">
<option value="100" {% if per_page==25 %}selected{% endif %}>100</option>
<option value="200" {% if per_page==50 %}selected{% endif %}>200</option>
<option value="300" {% if per_page==100 %}selected{% endif %}>300</option>
<option value="500" {% if per_page==500 %}selected{% endif %}>500</option>
<option value="750" {% if per_page==750 %}selected{% endif %}>750</option>
<option value="1000" {% if per_page==1000 %}selected{% endif %}>1000</option>
</select>
<input type="hidden" name="page" value="{{ page }}">
</form>
@@ -120,7 +156,8 @@
</li>
{% for p in range(1, total_pages + 1) %}
<li class="page-item {% if p == page %}active{% endif %}">
<a class="page-link" href="?{{ query_string }}{% if query_string %}&{% endif %}page={{ p }}">{{ p }}</a>
<a class="page-link" href="?{{ query_string }}{% if query_string %}&{% endif %}page={{ p }}">{{
p }}</a>
</li>
{% endfor %}
<li class="page-item {% if page >= total_pages %}disabled{% endif %}">
@@ -132,6 +169,7 @@
{% block scripts %}
<script src="{{ url_for('static_bp.serve_js', filename='product_suggestion.js') }}"></script>
<script src="{{ url_for('static_bp.serve_js', filename='table_search.js') }}"></script>
{% endblock %}
{% endblock %}

View File

@@ -3,91 +3,108 @@
{% block content %}
<div class="d-flex justify-content-between align-items-center flex-wrap mb-4">
<h2 class="mb-2">🗂 Masowa edycja kategorii list</h2>
<h2 class="mb-2">🗂 Masowa edycja kategorii</h2>
<div>
<a href="{{ url_for('admin_panel') }}" class="btn btn-outline-secondary">← Powrót do panelu</a>
</div>
</div>
<div class="alert alert-warning border-warning text-dark" role="alert">
<strong>Uwaga!</strong> Przypisanie więcej niż jednej kategorii do listy może zaburzyć
poprawne zliczanie wydatków, ponieważ wydatki tej listy będą jednocześnie
klasyfikowane do kilku kategorii.
<div class="card bg-dark text-white mb-5">
<div class="card-body">
<div class="alert alert-warning border-warning text-dark" role="alert">
⚠️ <strong>Uwaga!</strong> Przypisanie więcej niż jednej kategorii do listy może zaburzyć
poprawne zliczanie wydatków, ponieważ wydatki tej listy będą jednocześnie
klasyfikowane do kilku kategorii.
</div>
<form method="post">
<div class="card bg-dark text-white mb-5">
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-dark align-middle sortable">
<thead>
<tr>
<th scope="col">ID</th>
<th scope="col">Nazwa listy</th>
<th scope="col">Właściciel</th>
<th scope="col">Data utworzenia</th>
<th scope="col">Status</th>
<th scope="col">Podgląd produktów</th>
<th scope="col">Kategorie</th>
</tr>
</thead>
<tbody>
{% for l in lists %}
<tr>
<td>{{ l.id }}</td>
<td class="fw-bold align-middle">
<a href="{{ url_for('view_list', list_id=l.id) }}" class="text-white">{{ l.title
}}</a>
</td>
<td>
{% if l.owner %}
👤 {{ l.owner.username }} ({{ l.owner.id }})
{% else %}
-
{% endif %}
</td>
<td>{{ l.created_at.strftime('%Y-%m-%d %H:%M') if l.created_at else '-' }}</td>
<td>
{% if l.is_archived %}<span
class="badge rounded-pill bg-secondary">Archiwalna</span>{%
endif %}
{% if l.is_temporary %}<span
class="badge rounded-pill bg-warning text-dark">Tymczasowa</span>{%
endif %}
{% if l.is_public %}<span
class="badge rounded-pill bg-success">Publiczna</span>{% else
%}
<span class="badge rounded-pill bg-dark">Prywatna</span>{% endif %}
</td>
<td>
<button type="button" class="btn btn-sm btn-outline-light preview-btn"
data-list-id="{{ l.id }}">
🔍 Zobacz
</button>
</td>
<td style="min-width: 220px;">
<select name="categories_{{ l.id }}" multiple
class="form-select tom-dark bg-dark text-white border-secondary rounded">
{% for cat in categories %}
<option value="{{ cat.id }}" {% if cat in l.categories %}selected{% endif
%}>
{{ cat.name }}
</option>
{% endfor %}
</select>
</td>
</tr>
{% endfor %}
{% if lists|length == 0 %}
<tr>
<td colspan="12" class="text-center py-4">
Brak list zakupowych do wyświetlenia
</td>
</tr>
{% endif %}
</tbody>
</table>
</div>
</div>
</div>
<div>
<button type="submit" class="btn btn-sm btn-outline-light">💾 Zapisz zmiany</button>
</div>
</form>
</div>
</div>
<form method="post">
<div class="card bg-dark text-white mb-5">
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-dark table-striped align-middle sortable">
<thead>
<tr>
<th scope="col">ID</th>
<th scope="col">Nazwa listy</th>
<th scope="col">Właściciel</th>
<th scope="col">Data utworzenia</th>
<th scope="col">Status</th>
<th scope="col">Podgląd produktów</th>
<th scope="col">Kategorie</th>
</tr>
</thead>
<tbody>
{% for l in lists %}
<tr>
<td>{{ l.id }}</td>
<td class="fw-bold align-middle">
<a href="{{ url_for('view_list', list_id=l.id) }}" class="text-white">{{ l.title }}</a>
</td>
<td>
{% if l.owner %}
👤 {{ l.owner.username }} ({{ l.owner.id }})
{% else %}
-
{% endif %}
</td>
<td>{{ l.created_at.strftime('%Y-%m-%d %H:%M') if l.created_at else '-' }}</td>
<td>
{% if l.is_archived %}<span class="badge rounded-pill bg-secondary">Archiwalna</span>{%
endif %}
{% if l.is_temporary %}<span
class="badge rounded-pill bg-warning text-dark">Tymczasowa</span>{%
endif %}
{% if l.is_public %}<span class="badge rounded-pill bg-success">Publiczna</span>{% else
%}
<span class="badge rounded-pill bg-dark">Prywatna</span>{% endif %}
</td>
<td>
<button type="button" class="btn btn-sm btn-outline-info preview-btn"
data-list-id="{{ l.id }}">
🔍 Zobacz
</button>
</td>
<td style="min-width: 220px;">
<select name="categories_{{ l.id }}" multiple
class="form-select tom-dark bg-dark text-white border-secondary rounded">
{% for cat in categories %}
<option value="{{ cat.id }}" {% if cat in l.categories %}selected{% endif %}>
{{ cat.name }}
</option>
{% endfor %}
</select>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
<div class="mt-3">
<button type="submit" class="btn btn-success">💾 Zapisz</button>
</div>
</form>
<hr>
<div class="d-flex justify-content-between align-items-center mt-4">
<form method="get" class="d-flex align-items-center">
<label for="per_page" class="me-2">🔢 Pozycji na stronę:</label>
<select id="per_page" name="per_page" class="form-select form-select-sm me-2" onchange="this.form.submit()">
<select id="per_page" name="per_page" class="form-select form-select-sm me-2"
onchange="this.form.page.value = 1; this.form.submit();">
<option value="25" {% if per_page==25 %}selected{% endif %}>25</option>
<option value="50" {% if per_page==50 %}selected{% endif %}>50</option>
<option value="100" {% if per_page==100 %}selected{% endif %}>100</option>
@@ -103,7 +120,8 @@
</li>
{% for p in range(1, total_pages + 1) %}
<li class="page-item {% if p == page %}active{% endif %}">
<a class="page-link" href="?{{ query_string }}{% if query_string %}&{% endif %}page={{ p }}">{{ p }}</a>
<a class="page-link" href="?{{ query_string }}{% if query_string %}&{% endif %}page={{ p }}">{{
p }}</a>
</li>
{% endfor %}
<li class="page-item {% if page >= total_pages %}disabled{% endif %}">

View File

@@ -3,73 +3,128 @@
{% block content %}
<div class="d-flex justify-content-between align-items-center flex-wrap mb-4">
<h2 class="mb-2">📸 Wszystkie paragony</h2>
<h2 class="mb-2">
📸 {% if id == 'all' %}Wszystkie paragony{% else %}Paragony dla listy #{{ id }}{% endif %}
</h2>
<p class="text-white-50 small mt-1">
{% if id == 'all' %}
Rozmiar plików tej strony:
{% else %}
Rozmiar plików listy #{{ id }}:
{% endif %}
<strong>
{% if page_filesize >= 1024*1024 %}
{{ (page_filesize / 1024 / 1024) | round(2) }} MB
{% else %}
{{ (page_filesize / 1024) | round(1) }} kB
{% endif %}
</strong>
|
Łącznie:
<strong>
{% if total_filesize >= 1024*1024 %}
{{ (total_filesize / 1024 / 1024) | round(2) }} MB
{% else %}
{{ (total_filesize / 1024) | round(1) }} kB
{% endif %}
</strong>
</p>
<div>
<a href="{{ url_for('recalculate_filesizes_all') }}" class="btn btn-outline-primary me-2">
<a href="{{ url_for('recalculate_filesizes_all') }}" class="btn btn-outline-light me-2">
Przelicz rozmiary plików
</a>
<a href="{{ url_for('admin_panel') }}" class="btn btn-outline-secondary">← Powrót do panelu</a>
</div>
</div>
<div class="card bg-dark text-white mb-5">
<div class="card bg-secondary bg-opacity-10 text-white mb-5">
<div class="card-body">
<div class="row g-3">
{% for r in receipts %}
<div class="col-6 col-md-4 col-lg-3">
<div class="card bg-dark text-white h-100">
<a href="{{ url_for('uploaded_file', filename=r.filename) }}" class="glightbox" data-gallery="receipts"
data-title="{{ r.filename }}">
<img src="{{ url_for('uploaded_file', filename=r.filename) }}" class="card-img-top"
style="object-fit: cover; height: 200px;">
<div class="card bg-dark text-white h-100 shadow-sm border border-secondary">
<a href="{{ url_for('uploaded_file', filename=r.filename) }}?v={{ r.version_token or '0' }}" class="glightbox"
data-gallery="receipts" data-title="{{ r.filename }}">
<img src="{{ url_for('uploaded_file', filename=r.filename) }}?v={{ r.version_token or '0' }}"
class="card-img-top" style="object-fit: cover; height: 200px;"
title="Token: {{ r.version_token or '0' }}">
</a>
<div class="card-body text-center">
<p class="small text-truncate mb-1">{{ r.filename }}</p>
<p class="small mb-1">Wgrano: {{ r.uploaded_at.strftime('%Y-%m-%d %H:%M') }}</p>
{% if r.filesize and r.filesize >= 1024 * 1024 %}
<p class="small mb-1">Rozmiar: {{ (r.filesize / 1024 / 1024) | round(2) }} MB</p>
{% elif r.filesize %}
<p class="small mb-1">Rozmiar: {{ (r.filesize / 1024) | round(1) }} kB</p>
{% else %}
<p class="small mb-1 text-muted">Brak danych o rozmiarze</p>
{% endif %}
<a href="{{ url_for('rotate_receipt', receipt_id=r.id) }}"
class="btn btn-sm btn-outline-warning w-100 mb-2">🔄 Obróć o 90°</a>
<a href="#" class="btn btn-sm btn-outline-secondary w-100 mb-2" data-bs-toggle="modal"
data-bs-target="#adminCropModal" data-img-src="{{ url_for('uploaded_file', filename=r.filename) }}"
data-receipt-id="{{ r.id }}" data-crop-endpoint="{{ url_for('crop_receipt_admin') }}">
✂️ Przytnij
</a>
<a href="{{ url_for('rename_receipt', receipt_id=r.id) }}" class="btn btn-sm btn-outline-info w-100 mb-2">✏️
Zmień nazwę</a>
{% if not r.file_hash %}
<a href="{{ url_for('generate_receipt_hash', receipt_id=r.id) }}"
class="btn btn-sm btn-outline-secondary w-100 mb-2">🔐 Generuj hash</a>
{% endif %}
<a href="{{ url_for('delete_receipt', receipt_id=r.id) }}" class="btn btn-sm btn-outline-danger w-100 mb-2"
onclick="return confirm('Na pewno usunąć plik {{ r.filename }}?');">🗑️
Usuń</a>
<a href="{{ url_for('edit_list', list_id=r.list_id) }}" class="btn btn-sm btn-outline-light w-100 mb-2">✏️
Edytuj listę #{{ r.list_id }}</a>
<div class="card-body text-center p-2 small">
<div class="text-truncate fw-semibold" title="{{ r.filename }}">📄 {{ r.filename }}</div>
<div>📅 {{ r.uploaded_at.strftime('%Y-%m-%d %H:%M') }}</div>
<div>👤 {{ r.uploaded_by_user.username if r.uploaded_by_user else "?" }}</div>
<div>
💾
{% if r.filesize and r.filesize >= 1024 * 1024 %}
{{ (r.filesize / 1024 / 1024) | round(2) }} MB
{% elif r.filesize %}
{{ (r.filesize / 1024) | round(1) }} kB
{% else %}
Brak danych
{% endif %}
</div>
<div class="dropdown mt-2">
<button class="btn btn-sm btn-outline-light dropdown-toggle w-100" type="button"
data-bs-toggle="dropdown">
⋮ Akcje
</button>
<ul class="dropdown-menu dropdown-menu-dark w-100 text-start">
<li>
<a class="dropdown-item" href="{{ url_for('rotate_receipt', receipt_id=r.id) }}">🔄 Obróć o 90°</a>
</li>
<li>
<a class="dropdown-item" href="#" data-bs-toggle="modal" data-bs-target="#adminCropModal"
data-img-src="{{ url_for('uploaded_file', filename=r.filename) }}" data-receipt-id="{{ r.id }}"
data-crop-endpoint="{{ url_for('crop_receipt_admin') }}">✂️ Przytnij</a>
</li>
<li>
<a class="dropdown-item" href="{{ url_for('rename_receipt', receipt_id=r.id) }}">✏️ Zmień nazwę</a>
</li>
{% if not r.file_hash %}
<li>
<a class="dropdown-item" href="{{ url_for('generate_receipt_hash', receipt_id=r.id) }}">🔐 Generuj
hash</a>
</li>
{% endif %}
<li>
<a class="dropdown-item text-danger" href="{{ url_for('delete_receipt', receipt_id=r.id) }}"
onclick="return confirm('Na pewno usunąć plik {{ r.filename }}?');">🗑️ Usuń</a>
</li>
<li>
<hr class="dropdown-divider">
</li>
<li>
<a class="dropdown-item" href="{{ url_for('edit_list', list_id=r.list_id) }}">📋 Edytuj listę #{{
r.list_id }}</a>
</li>
</ul>
</div>
</div>
</div>
</div>
{% endfor %}
</div>
{% if not receipts %}
<div class="alert alert-info text-center mt-4" role="alert">
Nie wgrano żadnych paragonów.
<i class="fas fa-info-circle"></i>
Nie wgrano żadnego paragonu
</div>
{% endif %}
</div>
</div>
<hr>
<div class="d-flex justify-content-between align-items-center mt-4">
<form method="get" class="d-flex align-items-center">
<label for="per_page" class="me-2">🔢 Pozycji na stronę:</label>
<select id="per_page" name="per_page" class="form-select form-select-sm me-2" onchange="this.form.submit()">
<select id="per_page" name="per_page" class="form-select form-select-sm me-2"
onchange="this.form.page.value = 1; this.form.submit();">
<option value="25" {% if per_page==25 %}selected{% endif %}>25</option>
<option value="50" {% if per_page==50 %}selected{% endif %}>50</option>
<option value="100" {% if per_page==100 %}selected{% endif %}>100</option>
@@ -84,7 +139,8 @@
</li>
{% for p in range(1, total_pages + 1) %}
<li class="page-item {% if p == page %}active{% endif %}">
<a class="page-link" href="?{{ query_string }}{% if query_string %}&{% endif %}page={{ p }}">{{ p }}</a>
<a class="page-link" href="?{{ query_string }}{% if query_string %}&{% endif %}page={{ p }}">{{
p }}</a>
</li>
{% endfor %}
<li class="page-item {% if page >= total_pages %}disabled{% endif %}">
@@ -110,7 +166,7 @@
<div class="card-body text-center">
<p class="small mb-1 fw-bold">{{ f }}</p>
<div class="alert alert-warning small py-1 mb-2">Brak powiązania z listą!</div>
<a href="{{ url_for('delete_receipt', filename=f) }}" class="btn btn-sm btn-outline-danger w-100 mb-2"
<a href="{{ url_for('delete_receipt', filename=f) }}" class="btn btn-sm btn-outline-light w-100 mb-2"
onclick="return confirm('Na pewno usunąć WYŁĄCZNIE plik {{ f }} z dysku?');">
🗑 Usuń plik z serwera
</a>
@@ -132,8 +188,10 @@
<img id="adminCropImage" style="max-width: 100%; max-height: 100%; display: block; margin: auto;">
</div>
<div class="modal-footer">
<button class="btn btn-secondary" data-bs-dismiss="modal">Anuluj</button>
<button class="btn btn-success" id="adminSaveCrop">Zapisz</button>
<div class="btn-group" role="group">
<button type="button" class="btn btn-sm btn-outline-light" data-bs-dismiss="modal">❌ Anuluj</button>
<button type="button" class="btn btn-sm btn-outline-light" id="adminSaveCrop">💾 Zapisz</button>
</div>
<div id="adminCropLoading" class="position-absolute top-50 start-50 translate-middle text-center d-none">
<div class="spinner-border text-light" role="status"></div>
<div class="mt-2 text-light">⏳ Pracuję...</div>

View File

@@ -8,40 +8,47 @@
</div>
<!-- Formularz dodawania nowego użytkownika -->
<div class="card bg-dark text-white mb-4">
<div class="card bg-secondary bg-opacity-10 text-white mb-4">
<div class="card-body">
<h5 class="card-title"> Dodaj nowego użytkownika</h5>
<h5 class="card-title mb-3"> Dodaj nowego użytkownika</h5>
<form method="post" action="{{ url_for('add_user') }}">
<div class="row g-2">
<div class="row g-3 align-items-end">
<div class="col-md-4">
<input type="text" name="username" class="form-control bg-dark text-white border-secondary rounded"
placeholder="Nazwa użytkownika" required>
<label for="username" class="form-label text-white-50">Nazwa użytkownika</label>
<input type="text" id="username" name="username"
class="form-control bg-dark text-white border-secondary rounded" placeholder="np. jan" required>
</div>
<div class="col-md-4">
<input type="password" name="password" class="form-control bg-dark text-white border-secondary rounded"
placeholder="Hasło" required>
<label for="password" class="form-label text-white-50">Hasło</label>
<input type="password" id="password" name="password"
class="form-control bg-dark text-white border-secondary rounded" placeholder="min. 6 znaków" required>
</div>
<div class="col-md-4">
<button type="submit" class="btn btn-outline-success w-100">Dodaj użytkownika</button>
<div class="col-md-4 d-grid">
<button type="submit" class="btn btn-outline-light"> Dodaj użytkownika</button>
</div>
</div>
</form>
</div>
</div>
<div class="card bg-dark text-white mb-5">
<div class="card-body">
<table class="table table-dark table-striped align-middle">
<table class="table table-dark align-middle sortable">
<thead>
<tr>
<th>ID</th>
<th>Login</th>
<th>Rola</th>
<th>Listy</th>
<th>Produkty</th>
<th>Paragony</th>
<th>Akcje</th>
</tr>
</thead>
<tbody>
{% for user in users %}
{% for entry in user_data %}
{% set user = entry.user %}
<tr>
<td>{{ user.id }}</td>
<td class="fw-bold">{{ user.username }}</td>
@@ -52,17 +59,30 @@
<span class="badge rounded-pill bg-secondary">Użytkownik</span>
{% endif %}
</td>
<td>{{ entry.list_count }}</td>
<td>{{ entry.item_count }}</td>
<td>{{ entry.receipt_count }}</td>
<td>
<button class="btn btn-sm btn-outline-warning me-1" data-bs-toggle="modal"
<button class="btn btn-sm btn-outline-light me-1" data-bs-toggle="modal"
data-bs-target="#resetPasswordModal" data-user-id="{{ user.id }}" data-username="{{ user.username }}">
🔑 Ustaw hasło
</button>
{% if not user.is_admin %}
<a href="/admin/promote_user/{{ user.id }}" class="btn btn-sm btn-outline-info">⬆️ Ustaw admina</a>
<a href="/admin/promote_user/{{ user.id }}" class="btn btn-sm btn-outline-light">⬆️ Ustaw admina</a>
{% else %}
<a href="/admin/demote_user/{{ user.id }}" class="btn btn-sm btn-outline-secondary">⬇️ Usuń admina</a>
<a href="/admin/demote_user/{{ user.id }}" class="btn btn-sm btn-outline-light">⬇️ Usuń admina</a>
{% endif %}
{% if user.username == 'admin' %}
<a class="btn btn-sm btn-outline-light me-1 disabled" aria-disabled="true" tabindex="-1"
title="Nie można usunąć konta administratora-głównego.">
🗑️ Usuń
</a>
{% else %}
<a href="/admin/delete_user/{{ user.id }}" class="btn btn-sm btn-outline-light me-1"
onclick="return confirm('Czy na pewno chcesz usunąć użytkownika {{ user.username }}?\\n\\nWszystkie jego listy zostaną przeniesione na administratora.')">
🗑️ Usuń
</a>
{% endif %}
<a href="/admin/delete_user/{{ user.id }}" class="btn btn-sm btn-outline-danger me-1">🗑️ Usuń</a>
</td>
</tr>
{% endfor %}
@@ -70,6 +90,7 @@
</table>
</div>
</div>
<!-- Modal resetowania hasła -->
<div class="modal fade" id="resetPasswordModal" tabindex="-1" aria-labelledby="resetPasswordModalLabel"
aria-hidden="true">
@@ -82,10 +103,11 @@
</div>
<div class="modal-body">
<p id="resetUsernameLabel">Dla użytkownika: <strong></strong></p>
<input type="password" name="password" placeholder="Nowe hasło" class="form-control" required>
<input type="password" name="password" placeholder="Nowe hasło"
class="form-control bg-dark text-white border-secondary rounded" required>
</div>
<div class="modal-footer border-0">
<button type="submit" class="btn btn-success w-100">💾 Zapisz nowe hasło</button>
<button type="submit" class="btn btn-sm btn-outline-light w-100">💾 Zapisz nowe hasło</button>
</div>
</form>
</div>
@@ -93,7 +115,6 @@
</div>
{% block scripts %}
<script src="{{ url_for('static_bp.serve_js', filename='user_management.js') }}"></script>
{% endblock %}

View File

@@ -7,16 +7,19 @@
<title>{% block title %}Live Lista Zakupów{% endblock %}</title>
<link rel="icon" type="image/svg+xml" href="{{ url_for('favicon') }}">
{# --- Style CSS ładowane tylko dla niezablokowanych --- #}
{% if not is_blocked %}
{# --- Bootstrap i główny css zawsze --- #}
<link href="{{ url_for('static_bp.serve_css', filename='style.css') }}" rel="stylesheet">
<link href="{{ url_for('static_bp.serve_css_lib', filename='bootstrap.min.css') }}" rel="stylesheet">
{# --- Style CSS ładowane tylko dla niezablokowanych --- #}
{% set exclude_paths = ['/system-auth'] %}
{% if (exclude_paths | select("in", request.path) | list | length == 0)
and has_authorized_cookie
and not is_blocked %}
<link href="{{ url_for('static_bp.serve_css_lib', filename='glightbox.min.css') }}" rel="stylesheet">
<link href="{{ url_for('static_bp.serve_css_lib', filename='sort_table.min.css') }}" rel="stylesheet">
{% endif %}
{# --- Bootstrap zawsze --- #}
<link href="{{ url_for('static_bp.serve_css_lib', filename='bootstrap.min.css') }}" rel="stylesheet">
{# --- Cropper CSS tylko dla wybranych podstron --- #}
{% set substrings_cropper = ['/admin/receipts', '/edit_my_list'] %}
{% if substrings_cropper | select("in", request.path) | list | length > 0 %}
@@ -47,7 +50,7 @@
{% else %}
<div class="d-flex justify-content-center align-items-center text-white small flex-wrap text-center">
<span class="me-1">Przeglądasz jako</span>
<span class="badge rounded-pill bg-info">gość</span>
<span class="badge rounded-pill bg-info">niezalogowany/a</span>
</div>
{% endif %}
{% endif %}

View File

@@ -6,7 +6,7 @@
<a href="{{ url_for('main_page') }}" class="btn btn-outline-secondary">← Powrót</a>
</div>
<div class="card bg-dark text-white mb-5">
<div class="card bg-secondary bg-opacity-10 text-white mb-5">
<div class="card-body">
<form method="post">
@@ -88,8 +88,8 @@
<!-- Przyciski -->
<div class="btn-group mt-4" role="group">
<button type="submit" class="btn btn-outline-light">💾 Zapisz</button>
<a href="{{ url_for('main_page') }}" class="btn btn-outline-light">❌ Anuluj</a>
<button type="submit" class="btn btn-sm btn-outline-light">💾 Zapisz</button>
<a href="{{ url_for('main_page') }}" class="btn btn-sm btn-outline-light">❌ Anuluj</a>
</div>
</form>
@@ -103,33 +103,50 @@
<div class="row">
{% for r in receipts %}
<div class="col-6 col-md-4 col-lg-3">
<div class="card bg-dark text-white h-100">
<a href="{{ url_for('uploaded_file', filename=r.filename) }}" class="glightbox" data-gallery="receipts"
data-title="{{ r.filename }}">
<img src="{{ url_for('uploaded_file', filename=r.filename) }}" class="card-img-top"
style="object-fit: cover; height: 200px;">
<div class="card bg-dark text-white h-100 shadow-sm border border-secondary">
<a href="{{ url_for('uploaded_file', filename=r.filename) }}?v={{ r.version_token or '0' }}" class="glightbox"
data-gallery="receipts" data-title="{{ r.filename }}">
<img src="{{ url_for('uploaded_file', filename=r.filename) }}?v={{ r.version_token or '0' }}"
class="card-img-top" style="object-fit: cover; height: 200px;" title="{{ r.filename }}">
</a>
<div class="card-body text-center">
<p class="small text-truncate mb-1">{{ r.filename }}</p>
<p class="small mb-1">Wgrano: {{ r.uploaded_at.strftime('%Y-%m-%d %H:%M') }}</p>
{% if r.filesize and r.filesize >= 1024 * 1024 %}
<p class="small mb-1">Rozmiar: {{ (r.filesize / 1024 / 1024) | round(2) }} MB</p>
{% elif r.filesize %}
<p class="small mb-1">Rozmiar: {{ (r.filesize / 1024) | round(1) }} kB</p>
{% else %}
<p class="small mb-1 text-muted">Brak danych o rozmiarze</p>
{% endif %}
<a href="{{ url_for('rotate_receipt_user', receipt_id=r.id) }}"
class="btn btn-sm btn-outline-warning w-100 mb-2">🔄 Obróć o 90°</a>
<div class="card-body text-center p-2 small">
<div class="text-truncate fw-semibold" title="{{ r.filename }}">📄 {{ r.filename }}</div>
<div>📅 {{ r.uploaded_at.strftime('%Y-%m-%d %H:%M') }}</div>
<div>
💾
{% if r.filesize and r.filesize >= 1024 * 1024 %}
{{ (r.filesize / 1024 / 1024) | round(2) }} MB
{% elif r.filesize %}
{{ (r.filesize / 1024) | round(1) }} kB
{% else %}
Brak danych
{% endif %}
</div>
<a href="#" class="btn btn-sm btn-outline-secondary w-100 mb-2" data-bs-toggle="modal"
data-bs-target="#userCropModal" data-img-src="{{ url_for('uploaded_file', filename=r.filename) }}"
data-receipt-id="{{ r.id }}" data-crop-endpoint="{{ url_for('crop_receipt_user') }}">
✂️ Przytnij
</a>
<a href="{{ url_for('delete_receipt_user', receipt_id=r.id) }}" class="btn btn-sm btn-outline-danger w-100"
onclick="return confirm('Na pewno usunąć ten paragon?')">🗑️ Usuń</a>
<div class="dropdown mt-2">
<button class="btn btn-sm btn-outline-light dropdown-toggle w-100" type="button" data-bs-toggle="dropdown">
⋮ Akcje
</button>
<ul class="dropdown-menu dropdown-menu-dark w-100 text-start">
<li>
<a class="dropdown-item" href="{{ url_for('rotate_receipt_user', receipt_id=r.id) }}">🔄 Obróć o 90°</a>
</li>
<li>
<a class="dropdown-item" href="#" data-bs-toggle="modal" data-bs-target="#userCropModal"
data-img-src="{{ url_for('uploaded_file', filename=r.filename) }}" data-receipt-id="{{ r.id }}"
data-crop-endpoint="{{ url_for('crop_receipt_user') }}">
✂️ Przytnij
</a>
</li>
<li>
<a class="dropdown-item text-danger" href="{{ url_for('delete_receipt_user', receipt_id=r.id) }}"
onclick="return confirm('Na pewno usunąć ten paragon?')">
🗑️ Usuń
</a>
</li>
</ul>
</div>
</div>
</div>
</div>
@@ -140,7 +157,7 @@
<hr class="my-3">
<!-- Trigger przycisk -->
<div class="btn-group mt-4" role="group">
<button type="button" class="btn btn-outline-danger" data-bs-toggle="modal" data-bs-target="#deleteModal">
<button type="button" class="btn btn-sm btn-outline-danger" data-bs-toggle="modal" data-bs-target="#deleteModal">
🗑️ Usuń tę listę
</button>
</div>
@@ -156,13 +173,13 @@
</div>
<div class="modal-body">
<p>Aby usunąć listę <strong>{{ list.title }}</strong>, wpisz <code>usuń</code> i poczekaj 2 sekundy:</p>
<input type="text" id="confirm-delete-input" class="form-control bg-dark text-white border-warning"
placeholder="usuń">
<input type="text" id="confirm-delete-input" class="form-control bg-dark text-white border-warning rounded"
placeholder="">
</div>
<div class="modal-footer justify-content-between">
<div class="btn-group" role="group">
<button type="button" class="btn btn-outline-light" data-bs-dismiss="modal">Anuluj</button>
<button id="confirm-delete-btn" class="btn btn-outline-danger" disabled>🗑️ Usuń</button>
<button type="button" class="btn btn-sm btn-outline-light" data-bs-dismiss="modal">Anuluj</button>
<button id="confirm-delete-btn" class="btn btn-sm btn-outline-light" disabled>🗑️ Usuń</button>
</div>
</div>
</div>
@@ -182,8 +199,10 @@
<img id="userCropImage" style="max-width: 100%; max-height: 100%; display: block; margin: auto;">
</div>
<div class="modal-footer">
<button class="btn btn-secondary" data-bs-dismiss="modal">Anuluj</button>
<button class="btn btn-success" id="userSaveCrop">Zapisz</button>
<div class="btn-group" role="group">
<button type="button" class="btn btn-sm btn-outline-light" data-bs-dismiss="modal">❌ Anuluj</button>
<button type="button" class="btn btn-sm btn-outline-light" id="userSaveCrop">💾 Zapisz</button>
</div>
<div id="userCropLoading" class="position-absolute top-50 start-50 translate-middle text-center d-none">
<div class="spinner-border text-light" role="status"></div>
<div class="mt-2 text-light">⏳ Pracuję...</div>

View File

@@ -7,29 +7,33 @@
<a href="{{ url_for('main_page') }}" class="btn btn-outline-secondary">← Powrót</a>
</div>
<div class="d-flex justify-content-center mb-3">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="showAllLists" {% if show_all %}checked{% endif %}>
<label class="form-check-label ms-2 text-white" for="showAllLists">
Pokaż wszystkie publiczne listy innych
</label>
</div>
</div>
<div class="card bg-secondary bg-opacity-10 text-white mb-5">
<div class="card-body">
<div class="d-flex justify-content-center mb-3">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="showAllLists" {% if show_all %}checked{% endif %}>
<label class="form-check-label ms-2 text-white" for="showAllLists">
Pokaż wszystkie publiczne listy innych
</label>
</div>
</div>
<!-- Przyciski kategorii -->
<div class="d-flex flex-wrap gap-2 mb-3 justify-content-center">
<button type="button"
class="btn btn-sm category-filter {% if not selected_category %}btn-success{% else %}btn-outline-light{% endif %}"
data-category-id="">
🌐 Wszystkie
</button>
{% for cat in categories %}
<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 }}
</button>
{% endfor %}
<!-- Przyciski kategorii -->
<div class="d-flex flex-wrap gap-2 mb-3 justify-content-center">
<button type="button"
class="btn btn-sm category-filter {% if not selected_category %}btn-success{% else %}btn-outline-light{% endif %}"
data-category-id="">
🌐 Wszystkie
</button>
{% for cat in categories %}
<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 }}
</button>
{% endfor %}
</div>
</div>
</div>
<div class="card bg-dark text-white mb-5">
@@ -71,16 +75,7 @@
id="customStart">
<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="customEnd">
<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">
<label class="form-check-label ms-2 text-white" for="onlyWithExpenses">
Pokaż tylko listy z wydatkami
</label>
<button class="btn btn-outline-light" id="applyCustomRange">📊 Zastosuj zakres</button>
</div>
</div>
@@ -95,11 +90,13 @@
<!-- Tabela list z możliwością filtrowania -->
<div class="table-responsive">
<table class="table table-dark table-striped align-middle sortable">
<table class="table table-dark align-middle sortable">
<thead>
<tr>
<th></th>
<th>ID</th>
<th>Nazwa listy</th>
<th>Właściciel</th>
<th>Data</th>
<th>Wydatki (PLN)</th>
</tr>
@@ -115,11 +112,12 @@
<input type="checkbox" class="form-check-input list-checkbox"
data-amount="{{ '%.2f'|format(list.total_expense) }}">
</td>
<td>{{ list.id }}</td>
<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>👤 {{ list.owner_username or '?' }}</td>
<td>{{ list.created_at.strftime('%Y-%m-%d %H:%M') }}</td>
<td>{{ '%.2f'|format(list.total_expense) }}</td>
</tr>
{% endfor %}
@@ -145,8 +143,9 @@
<div class="d-flex flex-wrap gap-2 mb-3 justify-content-center">
<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="last30days">🗓️ Ostatnie 30
dni</button>
<button class="btn btn-outline-light range-btn active" 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>
@@ -160,7 +159,7 @@
<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>
<button class="btn btn-outline-light" id="customRangeBtn">📊 Pokaż dane z zakresu</button>
</div>
</div>
</div>
@@ -172,9 +171,9 @@
{% block scripts %}
<script src="{{ url_for('static_bp.serve_js_lib', filename='chart.js') }}"></script>
<script src="{{ url_for('static_bp.serve_js', filename='show_all_expense.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>
<script src="{{ url_for('static_bp.serve_js', filename='select_all_table.js') }}"></script>
{% endblock %}

View File

@@ -24,14 +24,13 @@
</a>
{% endif %}
</h2>
</div>
<a href="{{ request.url_root }}share/{{ list.share_token }}" class="btn btn-primary btn-sm w-100 mb-3" {% if not
list.is_public %}disabled{% endif %}>
✅ Otwórz tryb zakupowy / odznaczania produktów
</a>
<div id="share-card" class="card bg-dark text-white mb-4">
<div id="share-card" class="card bg-secondary bg-opacity-10 text-white mb-4">
<div class="card-body">
<div class="mb-2">
<strong id="share-header">
@@ -174,7 +173,7 @@
{% else %}
<li id="empty-placeholder" class="list-group-item bg-dark text-secondary text-center w-100">
Brak produktów w tej liście.
Brak produktów w tej liście.
</li>
{% endfor %}
</ul>
@@ -203,18 +202,19 @@
<h5 class="mt-4">📸 Paragony dodane do tej listy</h5>
<div class="row g-3 mt-2" id="receiptGallery">
{% if receipt_files %}
{% for file in receipt_files %}
{% if receipts %}
{% for r in receipts %}
<div class="col-6 col-md-4 col-lg-3 text-center">
<a href="{{ url_for('uploaded_file', filename=file) }}" class="glightbox" data-gallery="receipt-gallery">
<img src="{{ url_for('uploaded_file', filename=file) }}"
<a href="{{ url_for('uploaded_file', filename=r.filename) }}?v={{ r.version_token or '0' }}" class="glightbox"
data-gallery="receipt-gallery">
<img src="{{ url_for('uploaded_file', filename=r.filename) }}?v={{ r.version_token or '0' }}"
class="img-fluid rounded shadow-sm border border-secondary" style="max-height: 200px; object-fit: cover;">
</a>
</div>
{% endfor %}
{% else %}
<div class="alert alert-info text-center w-100" role="alert">
Brak wgranych paragonów do tej listy.
Brak wgranych paragonów do tej listy
</div>
{% endif %}
</div>
@@ -223,12 +223,24 @@
<div class="modal-dialog modal-lg modal-dialog-scrollable">
<div class="modal-content bg-dark text-white">
<div class="modal-header">
<h5 class="modal-title" id="massAddModalLabel">Masowe dodawanie produktów</h5>
<h5 class="modal-title" id="massAddModalLabel">
Masowe dodawanie produktów
<span id="massAddProductStats" class="badge rounded-pill bg-primary ms-2"></span>
</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Zamknij"></button>
</div>
<div class="modal-body">
<ul id="mass-add-list" class="list-group">
</ul>
<!-- SORTOWANIE i LICZNIK -->
<div id="sort-bar" class="mb-2"></div>
<div class="mb-2">
<span id="product-count" class="badge rounded-pill bg-primary ms-2"></span>
</div>
<!-- LISTA PRODUKTÓW -->
<ul id="mass-add-list" class="list-group"></ul>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-light" data-bs-dismiss="modal">Zamknij</button>
@@ -244,9 +256,7 @@
window.IS_SHARE = isShare;
window.LIST_ID = {{ list.id }};
window.IS_OWNER = {{ 'true' if is_owner else 'false' }};
</script>
<script src="{{ url_for('static_bp.serve_js', filename='mass_add.js') }}"></script>
<script src="{{ url_for('static_bp.serve_js', filename='receipt_upload.js') }}"></script>
<script src="{{ url_for('static_bp.serve_js', filename='sort_mode.js') }}"></script>

View File

@@ -132,38 +132,47 @@
data-bs-toggle="collapse" data-bs-target="#receiptSection" aria-expanded="false" aria-controls="receiptSection">
📄 Pokaż sekcję paragonów
</button>
<div class="collapse px-2 px-md-4" id="receiptSection">
{% set receipt_pattern = 'list_' ~ list.id %}
<div class="mt-3 p-3 border border-secondary rounded bg-dark text-white {% if not receipt_files %}d-none{% endif %}"
id="receiptAnalysisBlock">
<h5>🧠 Analiza paragonów (OCR)</h5>
<p class="text-small">System spróbuje automatycznie rozpoznać kwoty z dodanych paragonów.</p>
<div class="mt-3 p-3 border border-secondary rounded bg-dark text-white
{% if not receipts %}
d-none
{% endif %}" id="receiptAnalysisBlock">
<h5>🔍 Analiza paragonów (OCR)</h5>
<p class="text-small">System spróbuje automatycznie rozpoznać kwoty z dodanych paragonów.<br>
Dokonaj korekty jeśli źle rozpozna kwote i kliknij w "Dodaj" aby dodać wydatek.
</p>
{% if current_user.is_authenticated %}
<button id="analyzeBtn" class="btn btn-outline-info mb-3">
<button id="analyzeBtn" class="btn btn-sm btn-outline-light mb-3">
🔍 Zleć analizę OCR
</button>
{% else %}
<div class="alert alert-warning">🔒 Tylko zalogowani użytkownicy mogą zlecać analizę OCR paragonów.</div>
<div class="alert alert-warning text-centerg">
⚠️ Tylko zalogowani użytkownicy mogą zlecać analizę OCR.
</div>
{% endif %}
<div id="analysisResults" class="mt-2"></div>
</div>
<h5 class="mt-4">📸 Paragony dodane do tej listy</h5>
<div class="row g-3 mt-2" id="receiptGallery">
{% if receipt_files %}
{% for file in receipt_files %}
{% if receipts %}
{% for r in receipts %}
<div class="col-6 col-md-4 col-lg-3 text-center">
<a href="{{ url_for('uploaded_file', filename=file) }}" class="glightbox" data-gallery="receipt-gallery">
<img src="{{ url_for('uploaded_file', filename=file) }}"
<a href="{{ url_for('uploaded_file', filename=r.filename) }}?v={{ r.version_token or '0' }}" class="glightbox"
data-gallery="receipt-gallery">
<img src="{{ url_for('uploaded_file', filename=r.filename) }}?v={{ r.version_token or '0' }}"
class="img-fluid rounded shadow-sm border border-secondary" style="max-height: 200px; object-fit: cover;">
</a>
</div>
{% endfor %}
{% else %}
<div class="alert alert-info text-center w-100" role="alert">
Brak wgranych paragonów do tej listy.
Brak wgranych paragonów do tej listy
</div>
{% endif %}
</div>

View File

@@ -4,7 +4,7 @@
{% if not current_user.is_authenticated %}
<div class="alert alert-info text-center" role="alert">
Jesteś w trybie gościa. Możesz tylko przeglądać listy udostępnione publicznie.
Nie jesteś zalogowany/a. Możesz przeglądać tylko listy publiczne.
</div>
{% endif %}
@@ -13,9 +13,9 @@
<h2 class="mb-2">Stwórz nową listę</h2>
</div>
<div class="card bg-dark text-white mb-4">
<div class="card bg-secondary bg-opacity-10 text-white mb-4">
<div class="card-body">
<form action="/create" method="post">
<form action="{{ url_for('create_list') }}" method="post">
<div class="input-group mb-3">
<input type="text" name="title" id="title" placeholder="Wprowadź nazwę nowej listy" required
class="form-control bg-dark text-white border-secondary">
@@ -86,15 +86,18 @@
</span>
<div class="btn-group mt-2 mt-md-0" role="group">
<a href="/list/{{ l.id }}" class="btn btn-sm btn-outline-light">📄 Otwórz</a>
<a href="/copy/{{ l.id }}" class="btn btn-sm btn-outline-light">📋 Kopiuj</a>
<a href="/edit_my_list/{{ l.id }}" class="btn btn-sm btn-outline-light">✏️ Edytuj</a>
<a href="/toggle_archive_list/{{ l.id }}?archive=true" class="btn btn-sm btn-outline-light">🗄 Archiwizuj</a>
{% if l.is_public %}
<a href="/toggle_visibility/{{ l.id }}" class="btn btn-sm btn-outline-light">🙈 Ukryj</a>
{% else %}
<a href="/toggle_visibility/{{ l.id }}" class="btn btn-sm btn-outline-light">👁 Odkryj</a>
{% endif %}
<a href="{{ url_for('view_list', list_id=l.id) }}"
class="btn btn-sm btn-outline-light d-flex align-items-center text-nowrap">📄 Otwórz</a>
<a href="{{ url_for('shared_list', token=l.share_token) }}"
class="btn btn-sm btn-outline-light d-flex align-items-center text-nowrap"> Odznaczaj</a>
<a href="{{ url_for('copy_list', list_id=l.id) }}"
class="btn btn-sm btn-outline-light d-flex align-items-center text-nowrap">📋 Kopiuj</a>
<a href="{{ url_for('edit_my_list', list_id=l.id) }}"
class="btn btn-sm btn-outline-light d-flex align-items-center text-nowrap"> Ustawienia</a>
<a href="{{ url_for('toggle_visibility', list_id=l.id) }}"
class="btn btn-sm btn-outline-light d-flex align-items-center text-nowrap">
{% if l.is_public %}🙈 Ukryj{% else %}👁️ Odkryj{% endif %}
</a>
</div>
</div>
@@ -152,7 +155,8 @@
{% endfor %}
</span>
<a href="/guest-list/{{ l.id }}" class="btn btn-sm btn-outline-light">📄 Otwórz</a>
<a href="{{ url_for('shared_list', list_id=l.id) }}"
class="btn btn-sm btn-outline-light d-flex align-items-center text-nowrap">✏️ Odznaczaj</a>
</div>
<div class="progress progress-dark progress-thin mt-2 position-relative">
{# Kupione #}
@@ -200,14 +204,18 @@
{% for l in archived_lists %}
<li class="list-group-item bg-dark text-white d-flex justify-content-between align-items-center flex-wrap">
<span>{{ l.title }}</span>
<a href="/toggle_archive_list/{{ l.id }}?archive=false" class="btn btn-sm btn-outline-success">♻️
Przywróć</a>
<form action="{{ url_for('edit_my_list', list_id=l.id) }}" method="post" class="d-contents">
<input type="hidden" name="unarchive" value="1">
<button type="submit" class="btn btn-sm btn-outline-light">
♻️ Przywróć
</button>
</form>
</li>
{% endfor %}
</ul>
{% else %}
<div class="alert alert-info text-center" role="alert">
Nie masz żadnych zarchiwizowanych list.
Nie masz żadnych zarchiwizowanych list
</div>
{% endif %}
</div>