Merge pull request 'ukrycie_zaznaczonych' (#1) from ukrycie_zaznaczonych into master

Reviewed-on: #1
This commit is contained in:
gru
2025-07-12 23:39:35 +02:00
11 changed files with 443 additions and 120 deletions

View File

@@ -17,4 +17,7 @@ UPLOAD_FOLDER=uploads
AUTHORIZED_COOKIE_VALUE=twoj_wlasny_hash
# czas zycia cookie
AUTH_COOKIE_MAX_AGE=86400
AUTH_COOKIE_MAX_AGE=86400
# dla compose
HEALTHCHECK_TOKEN=alamapsaikota123

173
app.py
View File

@@ -35,6 +35,7 @@ UPLOAD_FOLDER = app.config.get('UPLOAD_FOLDER', 'uploads')
ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif', 'webp'}
AUTHORIZED_COOKIE_VALUE = app.config.get('AUTHORIZED_COOKIE_VALUE', '80d31cdfe63539c9')
AUTH_COOKIE_MAX_AGE = app.config.get('AUTH_COOKIE_MAX_AGE', 86400)
HEALTHCHECK_TOKEN = app.config.get('HEALTHCHECK_TOKEN', 'alamapsaikota1234')
os.makedirs(UPLOAD_FOLDER, exist_ok=True)
@@ -271,7 +272,7 @@ def require_system_password():
if endpoint is None:
return
if endpoint == 'system_auth':
if endpoint in ('system_auth', 'healthcheck'):
return
if 'authorized' not in request.cookies and not endpoint.startswith('login') and endpoint != 'favicon':
@@ -839,15 +840,27 @@ def delete_user(user_id):
flash('Użytkownik usunięty', 'success')
return redirect(url_for('list_users'))
@app.route('/admin/receipts')
@app.route('/admin/receipts/<id>')
@login_required
@admin_required
def admin_receipts():
def admin_receipts(id):
all_files = os.listdir(app.config['UPLOAD_FOLDER'])
image_files = [f for f in all_files if allowed_file(f)]
if id == "all":
filtered_files = image_files
else:
try:
list_id = int(id)
receipt_prefix = f"list_{list_id}_"
filtered_files = [f for f in image_files if f.startswith(receipt_prefix)]
except ValueError:
flash("Nieprawidłowe ID listy.", "danger")
return redirect(url_for('admin_panel'))
return render_template(
'admin/receipts.html',
image_files=image_files,
image_files=filtered_files,
upload_folder=app.config['UPLOAD_FOLDER']
)
@@ -861,6 +874,10 @@ def delete_receipt(filename):
flash('Plik usunięty', 'success')
else:
flash('Plik nie istnieje', 'danger')
next_url = request.args.get('next')
if next_url:
return redirect(next_url)
return redirect(url_for('admin_receipts'))
@app.route('/admin/delete_selected_lists', methods=['POST'])
@@ -879,16 +896,6 @@ def delete_selected_lists():
flash('Usunięto wybrane listy', 'success')
return redirect(url_for('admin_panel'))
@app.route('/admin/archive_list/<int:list_id>')
@login_required
@admin_required
def archive_list(list_id):
l = ShoppingList.query.get_or_404(list_id)
l.is_archived = True
db.session.commit()
flash('Lista oznaczona jako archiwalna', 'success')
return redirect(url_for('admin_panel'))
@app.route('/admin/delete_all_items')
@login_required
@admin_required
@@ -905,55 +912,115 @@ def edit_list(list_id):
l = ShoppingList.query.get_or_404(list_id)
expenses = Expense.query.filter_by(list_id=list_id).all()
total_expense = sum(e.amount for e in expenses)
users = User.query.all()
items = Item.query.filter_by(list_id=list_id).order_by(Item.id.desc()).all()
# Pobranie listy plików paragonów
receipt_pattern = f"list_{list_id}_"
all_files = os.listdir(app.config['UPLOAD_FOLDER'])
receipts = [f for f in all_files if f.startswith(receipt_pattern)]
if request.method == 'POST':
new_title = request.form.get('title')
new_amount_str = request.form.get('amount')
is_archived = 'archived' in request.form
new_owner_id = request.form.get('owner_id')
action = request.form.get('action')
if new_title and new_title.strip():
l.title = new_title.strip()
if action == 'save':
new_title = request.form.get('title', '').strip()
new_amount_str = request.form.get('amount')
is_archived = 'archived' in request.form
is_public = 'public' in request.form
new_owner_id = request.form.get('owner_id')
l.is_archived = is_archived
if new_title:
l.title = new_title
if new_owner_id:
try:
new_owner_id_int = int(new_owner_id)
if User.query.get(new_owner_id_int):
l.owner_id = new_owner_id_int
else:
flash('Wybrany użytkownik nie istnieje', 'danger')
l.is_archived = is_archived
l.is_public = is_public
if new_owner_id:
try:
new_owner_id_int = int(new_owner_id)
if User.query.get(new_owner_id_int):
l.owner_id = new_owner_id_int
else:
flash('Wybrany użytkownik nie istnieje', 'danger')
return redirect(url_for('edit_list', list_id=list_id))
except ValueError:
flash('Niepoprawny ID użytkownika', 'danger')
return redirect(url_for('edit_list', list_id=list_id))
except ValueError:
flash('Niepoprawny ID użytkownika', 'danger')
return redirect(url_for('edit_list', list_id=list_id))
if new_amount_str:
try:
new_amount = float(new_amount_str)
if expenses:
if new_amount_str:
try:
new_amount = float(new_amount_str)
for expense in expenses:
db.session.delete(expense)
db.session.commit()
new_expense = Expense(list_id=list_id, amount=new_amount)
db.session.add(new_expense)
db.session.commit()
except ValueError:
flash('Niepoprawna kwota', 'danger')
return redirect(url_for('edit_list', list_id=list_id))
new_expense = Expense(list_id=list_id, amount=new_amount)
db.session.add(new_expense)
db.session.commit()
flash('Zaktualizowano tytuł, właściciela, archiwizację i/lub kwotę wydatku', 'success')
except ValueError:
flash('Niepoprawna kwota', 'danger')
return redirect(url_for('edit_list', list_id=list_id))
else:
db.session.commit()
flash('Zaktualizowano tytuł, właściciela i/lub archiwizację', 'success')
flash('Zapisano zmiany listy', 'success')
return redirect(url_for('edit_list', list_id=list_id))
return redirect(url_for('admin_panel'))
elif action == 'add_item':
item_name = request.form.get('item_name', '').strip()
quantity_str = request.form.get('quantity', '1')
if not item_name:
flash('Podaj nazwę produktu', 'danger')
return redirect(url_for('edit_list', list_id=list_id))
return render_template('admin/edit_list.html', list=l, total_expense=total_expense, users=users)
try:
quantity = int(quantity_str)
if quantity < 1:
quantity = 1
except ValueError:
quantity = 1
new_item = Item(list_id=list_id, name=item_name, quantity=quantity, added_by=current_user.id)
db.session.add(new_item)
if not SuggestedProduct.query.filter(func.lower(SuggestedProduct.name) == item_name.lower()).first():
db.session.add(SuggestedProduct(name=item_name))
db.session.commit()
flash('Dodano produkt', 'success')
return redirect(url_for('edit_list', list_id=list_id))
elif action == 'delete_item':
item_id = request.form.get('item_id')
item = Item.query.get(item_id)
if item and item.list_id == list_id:
db.session.delete(item)
db.session.commit()
flash('Usunięto produkt', 'success')
else:
flash('Nie znaleziono produktu', 'danger')
return redirect(url_for('edit_list', list_id=list_id))
elif action == 'toggle_purchased':
item_id = request.form.get('item_id')
item = Item.query.get(item_id)
if item and item.list_id == list_id:
item.purchased = not item.purchased
db.session.commit()
flash('Zmieniono status oznaczenia produktu', 'success')
else:
flash('Nie znaleziono produktu', 'danger')
return redirect(url_for('edit_list', list_id=list_id))
# Przekazanie receipts do szablonu
return render_template(
'admin/edit_list.html',
list=l,
total_expense=total_expense,
users=users,
items=items,
receipts=receipts,
upload_folder=app.config['UPLOAD_FOLDER']
)
@app.route('/admin/products')
@login_required
@@ -974,7 +1041,6 @@ def list_products():
suggestions_dict=suggestions_dict
)
@app.route('/admin/sync_suggestion/<int:item_id>', methods=['POST'])
@login_required
def sync_suggestion_ajax(item_id):
@@ -1136,6 +1202,15 @@ def demote_user(user_id):
flash(f'Użytkownik {user.username} został zdegradowany.', 'success')
return redirect(url_for('list_users'))
@app.route('/healthcheck')
def healthcheck():
header_token = request.headers.get("X-Internal-Check")
correct_token = app.config.get('HEALTHCHECK_TOKEN')
if header_token != correct_token:
abort(404)
return 'OK', 200
# =========================================================================================
# SOCKET.IO
# =========================================================================================

View File

@@ -9,4 +9,5 @@ class Config:
DEFAULT_ADMIN_PASSWORD = os.environ.get('DEFAULT_ADMIN_PASSWORD', 'admin123')
UPLOAD_FOLDER = os.environ.get('UPLOAD_FOLDER', 'uploads')
AUTHORIZED_COOKIE_VALUE = os.environ.get('AUTHORIZED_COOKIE_VALUE', 'cookievalue')
AUTH_COOKIE_MAX_AGE = int(os.environ.get('AUTH_COOKIE_MAX_AGE', 86400))
AUTH_COOKIE_MAX_AGE = int(os.environ.get('AUTH_COOKIE_MAX_AGE', 86400))
HEALTHCHECK_TOKEN = os.environ.get('HEALTHCHECK_TOKEN', 'alamapsaikota1234')

View File

@@ -4,6 +4,12 @@ services:
container_name: live-lista-zakupow
ports:
- "${APP_PORT:-8000}:8000"
healthcheck:
test: ["CMD", "python", "-c", "import urllib.request; import sys; req = urllib.request.Request('http://localhost:8000/healthcheck', headers={'X-Internal-Check': '${HEALTHCHECK_TOKEN}'}); sys.exit(0) if urllib.request.urlopen(req).read() == b'OK' else sys.exit(1)"]
interval: 30s
timeout: 10s
retries: 3
start_period: 10s
environment:
- FLASK_APP=app.py
- FLASK_ENV=production
@@ -14,5 +20,6 @@ services:
- UPLOAD_FOLDER=${UPLOAD_FOLDER}
- AUTHORIZED_COOKIE_VALUE=${AUTHORIZED_COOKIE_VALUE}
- AUTH_COOKIE_MAX_AGE=${AUTH_COOKIE_MAX_AGE}
- HEALTHCHECK_TOKEN=${HEALTHCHECK_TOKEN}
volumes:
- .:/app

View File

@@ -186,11 +186,12 @@ input.form-control {
box-sizing: border-box;
}
@media (max-width: 600px) {
@media (max-width: 768px) {
.info-bar-fixed {
position: static;
font-size: 0.85rem;
padding: 8px 4px;
border-radius: 10px 10px 0 0;
border-radius: 0;
}
}
@@ -264,4 +265,18 @@ input.form-control {
#empty-placeholder {
font-style: italic;
pointer-events: none;
}
#items li.hide-purchased {
display: none !important;
}
.list-group-item:first-child,
.list-group-item:last-child {
border-radius: 0 !important;
}
.fade-out {
opacity: 0;
transition: opacity 0.5s ease;
}

View File

@@ -16,9 +16,10 @@ function updateItemState(itemId, isChecked) {
if (sp) sp.remove();
}
updateProgressBar();
applyHidePurchased();
}
function updateProgressBar() {
/* function updateProgressBar() {
const items = document.querySelectorAll('#items li');
const total = items.length;
const purchased = Array.from(items).filter(li => li.classList.contains('bg-success')).length;
@@ -30,6 +31,48 @@ function updateProgressBar() {
progressBar.setAttribute('aria-valuenow', percent);
progressBar.textContent = `${percent}%`;
}
} */
function updateProgressBar() {
const items = document.querySelectorAll('#items li');
const total = items.length;
const purchased = Array.from(items).filter(li => li.classList.contains('bg-success')).length;
const percent = total > 0 ? Math.round((purchased / total) * 100) : 0;
// Pasek postępu
const progressBar = document.getElementById('progress-bar');
if (progressBar) {
progressBar.style.width = `${percent}%`;
progressBar.setAttribute('aria-valuenow', percent);
progressBar.textContent = percent > 0 ? `${percent}%` : ''; // opcjonalnie
}
// Label na pasku postępu
const progressLabel = document.getElementById('progress-label');
if (progressLabel) {
progressLabel.textContent = `${percent}%`;
if (percent === 0) {
progressLabel.style.display = 'inline';
} else {
progressLabel.style.display = 'none';
}
// Kolor tekstu labela
if (percent < 50) {
progressLabel.classList.remove('text-dark');
progressLabel.classList.add('text-white');
} else {
progressLabel.classList.remove('text-white');
progressLabel.classList.add('text-dark');
}
}
// Nagłówek
const purchasedCount = document.getElementById('purchased-count');
if (purchasedCount) purchasedCount.textContent = purchased;
const totalCount = document.getElementById('total-count');
if (totalCount) totalCount.textContent = total;
const percentValue = document.getElementById('percent-value');
if (percentValue) percentValue.textContent = percent;
}
function addItem(listId) {
@@ -91,6 +134,22 @@ function submitExpense(listId) {
}
function copyLink(link) {
if (navigator.share) {
navigator.share({
title: 'Udostępnij link',
text: 'Link do listy::',
url: link
}).then(() => {
showToast('Link udostępniony!');
}).catch((err) => {
tryClipboard(link);
});
return;
}
tryClipboard(link);
}
function tryClipboard(link) {
if (navigator.clipboard && window.isSecureContext) {
navigator.clipboard.writeText(link).then(() => {
showToast('Link skopiowany do schowka!');
@@ -103,33 +162,6 @@ function copyLink(link) {
}
}
/* function shareLink(link) {
if (navigator.share) {
navigator.share({
title: 'Udostępnij moją listę',
text: 'Zobacz tę listę!',
url: link
})
.catch((error) => {
console.error('Błąd podczas udostępniania', error);
alert('Nie udało się udostępnić linka');
});
} else {
copyLink(link);
}
}
function fallbackCopy(link) {
navigator.clipboard.writeText(link).then(() => {
alert('Link skopiowany do schowka!');
});
}
*/
function openList(link) {
window.open(link, '_blank');
}
function fallbackCopyText(text) {
const textarea = document.createElement('textarea');
textarea.value = text;
@@ -156,6 +188,48 @@ function fallbackCopyText(text) {
document.body.removeChild(textarea);
}
function openList(link) {
window.open(link, '_blank');
}
function applyHidePurchased(isInit = false) {
console.log("applyHidePurchased: wywołana, isInit =", isInit);
const toggle = document.getElementById('hidePurchasedToggle');
if (!toggle) return;
const hide = toggle.checked;
const items = document.querySelectorAll('#items li');
items.forEach(li => {
const isPurchased = li.classList.contains('bg-success');
if (isPurchased) {
if (hide) {
if (isInit) {
// Jeśli inicjalizacja: od razu ukryj
li.classList.add('hide-purchased');
li.classList.remove('fade-out');
} else {
// Z animacją
li.classList.add('fade-out');
setTimeout(() => {
li.classList.add('hide-purchased');
}, 700);
}
} else {
// Odsłanianie
li.classList.remove('hide-purchased');
setTimeout(() => {
li.classList.remove('fade-out');
}, 10);
}
} else {
// Element niekupiony — zawsze pokazany
li.classList.remove('hide-purchased', 'fade-out');
}
});
}
function toggleVisibility(listId) {
fetch('/toggle_visibility/' + listId, {method: 'POST'})
.then(response => response.json())
@@ -282,6 +356,7 @@ function updateListSmoothly(newItems) {
updateProgressBar();
toggleEmptyPlaceholder();
applyHidePurchased();
}
document.addEventListener("DOMContentLoaded", function() {
@@ -302,3 +377,16 @@ document.addEventListener("DOMContentLoaded", function() {
localStorage.setItem("receiptSectionOpen", "false");
});
});
document.addEventListener("DOMContentLoaded", function() {
const toggle = document.getElementById('hidePurchasedToggle');
if (!toggle) return;
const savedState = localStorage.getItem('hidePurchasedToggle');
toggle.checked = savedState === 'true';
applyHidePurchased(true);
toggle.addEventListener('change', function() {
localStorage.setItem('hidePurchasedToggle', toggle.checked ? 'true' : 'false');
applyHidePurchased();
});
});

View File

@@ -20,7 +20,7 @@
<a class="nav-link" href="/admin/users">👥 Zarządzanie użytkownikami</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/admin/receipts">📸 Paragony</a>
<a class="nav-link" href="/admin/receipts/all">📸 Paragony</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/admin/products">🛍️ Produkty</a>
@@ -141,7 +141,6 @@
</td>
<td class="d-flex flex-wrap gap-1">
<a href="{{ url_for('edit_list', list_id=l.id) }}" class="btn btn-sm btn-outline-primary">✏️ Edytuj</a>
<a href="{{ url_for('archive_list', list_id=l.id) }}" class="btn btn-sm btn-outline-secondary">📥 Archiwizuj</a>
<a href="{{ url_for('delete_list', list_id=l.id) }}" class="btn btn-sm btn-outline-danger">🗑️ Usuń</a>
</td>
</tr>

View File

@@ -3,43 +3,164 @@
{% block content %}
<div class="d-flex justify-content-between align-items-center flex-wrap mb-4">
<h2 class="mb-2"> Edytuj listę #{{ list.id }}</h2>
<h2 class="mb-2">🛠 Edytuj listę #{{ list.id }}</h2>
<a href="{{ url_for('admin_panel') }}" class="btn btn-outline-secondary">← Powrót</a>
</div>
<form method="post">
<div class="mb-4">
<label for="title" class="form-label">Ustaw nazwę</label>
<input type="text" class="form-control bg-dark text-white border-secondary rounded" id="title" name="title" value="{{ list.title }}" required>
</div>
<div class="card bg-dark text-white mb-5">
<div class="card-body">
<h4 class="card-title">📄 Podstawowe informacje</h4>
<form method="post" class="mt-3">
<input type="hidden" name="action" value="save">
<div class="mb-3">
<label for="title" class="form-label">Nazwa listy</label>
<input type="text" class="form-control bg-dark text-white border-secondary rounded" id="title" name="title" value="{{ list.title }}" required>
</div>
<div class="mb-4">
<label for="amount" class="form-label">Ustaw kwotę wydatku (PLN)</label>
<input type="number" step="0.01" min="0" class="form-control bg-dark text-white border-secondary rounded" id="amount" name="amount" value="{{ '%.2f'|format(total_expense) }}">
</div>
<div class="mb-3">
<label for="amount" class="form-label">Całkowity wydatek (PLN)</label>
<input type="number" step="0.01" min="0" class="form-control bg-dark text-white border-secondary rounded" id="amount" name="amount" value="{{ '%.2f'|format(total_expense) }}">
</div>
<div class="mb-4">
<label for="owner_id" class="form-label">Zmień właściciela</label>
<select class="form-select bg-dark text-white border-secondary rounded" id="owner_id" name="owner_id">
{% for user in users %}
<option value="{{ user.id }}" {% if list.owner_id == user.id %}selected{% endif %}>
{{ user.username }}
</option>
<div class="mb-3">
<label for="owner_id" class="form-label">Właściciel</label>
<select class="form-select bg-dark text-white border-secondary" id="owner_id" name="owner_id">
{% for user in users %}
<option value="{{ user.id }}" {% if list.owner_id == user.id %}selected{% endif %}>{{ user.username }}</option>
{% endfor %}
</select>
</div>
<div class="form-check form-switch mb-3">
<input class="form-check-input" type="checkbox" id="archived" name="archived" {% if list.is_archived %}checked{% endif %}>
<label class="form-check-label" for="archived">Archiwalna</label>
</div>
<div class="form-check form-switch mb-4">
<input class="form-check-input" type="checkbox" id="public" name="public" {% if list.is_public %}checked{% endif %}>
<label class="form-check-label" for="public">Publiczna</label>
</div>
<div class="mb-4">
<label class="form-label">Link do udostępnienia</label>
<input type="text" class="form-control bg-dark text-white border-secondary rounded" readonly value="{{ request.url_root }}share/{{ list.share_token }}">
</div>
<button type="submit" class="btn btn-success me-2">💾 Zapisz zmiany</button>
</form>
</div>
</div>
<div class="card bg-dark text-white mb-5">
<div class="card-body">
<h4 class="card-title">🛒 Produkty</h4>
<form method="post" class="row g-2 mb-3">
<input type="hidden" name="action" value="add_item">
<div class="col-md-8">
<input type="text" class="form-control bg-dark text-white border-secondary rounded" name="item_name" placeholder="Nazwa produktu" required>
</div>
<div class="col-md-1">
<input type="number" class="form-control bg-dark text-white border-secondary rounded" name="quantity" min="1" value="1">
</div>
<div class="col-md-3 d-grid">
<button type="submit" class="btn btn-outline-success"> Dodaj</button>
</div>
</form>
<div class="table-responsive">
<table class="table table-dark table-bordered align-middle">
<thead>
<tr>
<th scope="col">Nazwa</th>
<th scope="col">Status</th>
<th scope="col">Oznaczenie</th>
<th scope="col">Usuń</th>
</tr>
</thead>
<tbody>
{% for item in items %}
<tr>
<td>
<strong>{{ item.name }}</strong>
<small class="text-muted">(x{{ item.quantity }})</small>
</td>
<td>
{% if item.purchased %}
<span class="badge bg-success">✔️ Kupiony</span>
{% else %}
<span class="badge bg-secondary">Nieoznaczony</span>
{% endif %}
</td>
<td>
<form method="post" action="{{ url_for('edit_list', list_id=list.id) }}" class="d-inline">
<input type="hidden" name="action" value="toggle_purchased">
<input type="hidden" name="item_id" value="{{ item.id }}">
{% if item.purchased %}
<button type="submit" class="btn btn-outline-warning btn-sm w-100">🚫 Odznacz</button>
{% else %}
<button type="submit" class="btn btn-outline-success btn-sm w-100">✅ Oznacz</button>
{% endif %}
</form>
</td>
<td>
<form method="post" action="{{ url_for('edit_list', list_id=list.id) }}" class="d-inline">
<input type="hidden" name="action" value="delete_item">
<input type="hidden" name="item_id" value="{{ item.id }}">
<button type="submit" class="btn btn-danger btn-sm w-100">🗑️ Usuń</button>
</form>
</td>
</tr>
{% else %}
<tr>
<td colspan="4" class="text-center text-muted">Brak produktów.</td>
</tr>
{% endfor %}
</select>
</div>
</tbody>
</table>
</div>
<div class="form-check form-switch mb-4">
<input class="form-check-input" type="checkbox" id="archived" name="archived" {% if list.is_archived %}checked{% endif %}>
<label class="form-check-label" for="archived">
Lista archiwalna
</label>
</div>
</div>
<div class="mb-2">
<button type="submit" class="btn btn-success me-2">💾 Zapisz</button>
<a href="{{ url_for('admin_panel') }}" class="btn btn-secondary">Anuluj</a>
<div class="card bg-dark text-white mb-5">
<div class="card-body">
<h4 class="card-title">🧾 Paragony</h4>
<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
</a>
</div>
<div class="row g-3">
{% for img in receipts %}
{% set file_path = upload_folder ~ '/' ~ img %}
{% set file_size = (file_path | filesizeformat) %}
{% set upload_time = (file_path | filemtime) %}
<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=img) }}" data-lightbox="receipts" data-title="{{ img }}" class="glightbox">
<img src="{{ url_for('uploaded_file', filename=img) }}" class="card-img-top" style="object-fit: cover; height: 200px;">
</a>
<div class="card-body text-center">
<p class="small text-truncate mb-1">{{ img }}</p>
<p class="small mb-1">Rozmiar: {{ file_size }}</p>
<p class="small mb-1">Wgrano: {{ upload_time.strftime('%Y-%m-%d %H:%M') }}</p>
<a href="{{ url_for('delete_receipt', filename=img) }}?next={{ url_for('edit_list', list_id=list.id) }}" class="btn btn-sm btn-outline-danger w-100">🗑️ Usuń</a>
</div>
</div>
</div>
{% endfor %}
</div>
{% if not receipts %}
<div class="alert alert-info text-center mt-3" role="alert">
Brak paragonów.
</div>
{% endif %}
</div>
</form>
</div>
{% endblock %}

View File

@@ -10,12 +10,12 @@
<div class="row g-3">
{% for img in image_files %}
{% set list_id = img.split('_')[1] if '_' in img else None %}
{% set file_path = (upload_folder ~ '/' ~ img) %}
{% set file_path = upload_folder ~ '/' ~ img %}
{% set file_size = (file_path | filesizeformat) %}
{% set upload_time = (file_path | filemtime) %}
<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=img) }}" data-lightbox="receipts" data-title="{{ img }}">
<a href="{{ url_for('uploaded_file', filename=img) }}" class="glightbox" data-gallery="receipts" data-title="{{ img }}">
<img src="{{ url_for('uploaded_file', filename=img) }}" class="card-img-top" style="object-fit: cover; height: 200px;">
</a>
<div class="card-body text-center">
@@ -23,9 +23,9 @@
<p class="small mb-1">Rozmiar: {{ file_size }}</p>
<p class="small mb-1">Wgrano: {{ upload_time.strftime('%Y-%m-%d %H:%M') }}</p>
{% if list_id %}
<a href="{{ url_for('view_list', list_id=list_id|int) }}" class="btn btn-sm btn-outline-light w-100 mb-2">🔗 Lista #{{ list_id }}</a>
<a href="{{ url_for('edit_list', list_id=list_id|int) }}" class="btn btn-sm btn-outline-light w-100 mb-2">✏️ Edytuj listę #{{ list_id }}</a>
{% endif %}
<a href="{{ url_for('delete_receipt', filename=img) }}" class="btn btn-sm btn-outline-danger w-100">🗑️ Usuń</a>
<a href="{{ url_for('delete_receipt', filename=img) }}?next={{ request.path }}" class="btn btn-sm btn-outline-danger w-100">🗑️ Usuń</a>
</div>
</div>
</div>
@@ -33,8 +33,8 @@
</div>
{% if not image_files %}
<div class="alert alert-info text-center" role="alert">
Nie wgrano paragonów.
<div class="alert alert-info text-center mt-4" role="alert">
Nie wgrano żadnych paragonów.
</div>
{% endif %}

View File

@@ -50,11 +50,13 @@ Lista: <strong>{{ list.title }}</strong>
<!-- Progress bar (dynamic) -->
<h5 id="progress-title" class="mb-2">
📊 Postęp listy — {{ purchased_count }}/{{ total_count }} kupionych ({{ percent|round(0) }}%)
📊 Postęp listy —
<span id="purchased-count">{{ purchased_count }}</span>/
<span id="total-count">{{ total_count }}</span> kupionych
(<span id="percent-value">{{ percent|round(0) }}</span>%)
</h5>
<div class="progress progress-dark position-relative">
{# właściwy pasek postępu #}
<div id="progress-bar"
class="progress-bar bg-warning text-dark"
role="progressbar"
@@ -62,7 +64,7 @@ Lista: <strong>{{ list.title }}</strong>
aria-valuenow="{{ percent }}" aria-valuemin="0" aria-valuemax="100">
</div>
<span class="progress-label small fw-bold
<span id="progress-label" class="progress-label small fw-bold
{% if percent < 50 %}text-white{% else %}text-dark{% endif %}">
{{ percent|round(0) }}%
</span>
@@ -78,6 +80,11 @@ Lista: <strong>{{ list.title }}</strong>
</div>
{% endif %}
<div class="form-check form-switch mb-3 d-flex justify-content-end">
<input class="form-check-input" type="checkbox" id="hidePurchasedToggle">
<label class="form-check-label ms-2" for="hidePurchasedToggle">Ukryj zaznaczone</label>
</div>
<ul id="items" class="list-group mb-3">
{% for item in items %}
<li data-name="{{ item.name|lower }}" id="item-{{ item.id }}" class="list-group-item d-flex justify-content-between align-items-center flex-wrap {% if item.purchased %}bg-success text-white{% else %}item-not-checked{% endif %}" id="item-{{ item.id }}">

View File

@@ -4,6 +4,8 @@
<h2 class="mb-2">
🛍️ {{ list.title }}
{% if list.is_archived %}
<span class="badge bg-secondary ms-2">(Archiwalna)</span>
{% endif %}
@@ -18,6 +20,11 @@
{% endif %}
</h2>
<div class="form-check form-switch mb-3 d-flex justify-content-end">
<input class="form-check-input" type="checkbox" id="hidePurchasedToggle">
<label class="form-check-label ms-2" for="hidePurchasedToggle">Ukryj zaznaczone</label>
</div>
<ul id="items" class="list-group mb-3">
{% for item in items %}
<li data-name="{{ item.name|lower }}" id="item-{{ item.id }}" class="list-group-item d-flex justify-content-between align-items-center flex-wrap clickable-item {% if item.purchased %}bg-success text-white{% else %}item-not-checked{% endif %}" id="item-{{ item.id }}"> <div class="d-flex align-items-center gap-3 flex-grow-1">