sortowanie_w_mass_add #10
3
.gitattributes
vendored
Normal file
3
.gitattributes
vendored
Normal 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
6
.gitignore
vendored
@@ -3,9 +3,9 @@ venv
|
||||
env
|
||||
*.db
|
||||
__pycache__
|
||||
instance/
|
||||
database/
|
||||
uploads/
|
||||
.DS_Store
|
||||
db/*
|
||||
db/mysql/*
|
||||
db/pgsql/*
|
||||
db/shopping.db
|
||||
*.swp
|
@@ -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");
|
||||
}
|
||||
});
|
||||
|
@@ -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();
|
||||
|
@@ -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';
|
||||
|
@@ -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 });
|
||||
};
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
||||
|
@@ -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(() => {
|
||||
|
@@ -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>`;
|
||||
|
17
static/js/show_all_expense.js
Normal file
17
static/js/show_all_expense.js
Normal 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
28
static/js/table_search.js
Normal 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();
|
||||
});
|
||||
}
|
||||
});
|
@@ -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 %}
|
@@ -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>
|
||||
|
@@ -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 %}
|
@@ -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 %}">
|
||||
|
@@ -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>
|
||||
|
@@ -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 %}
|
||||
|
||||
|
@@ -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 %}
|
||||
|
@@ -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>
|
||||
|
@@ -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 %}
|
@@ -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>
|
||||
|
@@ -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>
|
||||
|
@@ -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>
|
||||
|
Reference in New Issue
Block a user