zmiany ux i w panelu
This commit is contained in:
@ -6,4 +6,7 @@ CREATE TABLE IF NOT EXISTS suggested_product (
|
||||
|
||||
# NOTATKI
|
||||
ALTER TABLE item
|
||||
ADD COLUMN note TEXT;
|
||||
ADD COLUMN note TEXT;
|
||||
|
||||
# NOWE FUNKCJE ADMINA
|
||||
ALTER TABLE shopping_list ADD COLUMN is_archived BOOLEAN DEFAULT FALSE;
|
||||
|
144
app.py
144
app.py
@ -13,6 +13,8 @@ from config import Config
|
||||
from PIL import Image
|
||||
from werkzeug.utils import secure_filename
|
||||
from werkzeug.middleware.proxy_fix import ProxyFix
|
||||
from sqlalchemy import func
|
||||
|
||||
|
||||
app = Flask(__name__)
|
||||
app.config.from_object(Config)
|
||||
@ -47,6 +49,7 @@ class ShoppingList(db.Model):
|
||||
share_token = db.Column(db.String(64), unique=True, nullable=True)
|
||||
expires_at = db.Column(db.DateTime, nullable=True)
|
||||
owner = db.relationship('User', backref='lists', lazy=True)
|
||||
is_archived = db.Column(db.Boolean, default=False)
|
||||
|
||||
class Item(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
@ -77,6 +80,14 @@ app.register_blueprint(static_bp)
|
||||
def allowed_file(filename):
|
||||
return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
|
||||
|
||||
def get_progress(list_id):
|
||||
items = Item.query.filter_by(list_id=list_id).all()
|
||||
total_count = len(items)
|
||||
purchased_count = len([i for i in items if i.purchased])
|
||||
percent = (purchased_count / total_count * 100) if total_count > 0 else 0
|
||||
return purchased_count, total_count, percent
|
||||
|
||||
|
||||
@login_manager.user_loader
|
||||
def load_user(user_id):
|
||||
return User.query.get(int(user_id))
|
||||
@ -195,12 +206,24 @@ def view_list(list_id):
|
||||
shopping_list = ShoppingList.query.get_or_404(list_id)
|
||||
items = Item.query.filter_by(list_id=list_id).all()
|
||||
|
||||
total_count = len(items)
|
||||
purchased_count = len([i for i in items if i.purchased])
|
||||
percent = (purchased_count / total_count * 100) if total_count > 0 else 0
|
||||
|
||||
receipt_pattern = f"list_{list_id}"
|
||||
all_files = os.listdir(app.config['UPLOAD_FOLDER'])
|
||||
|
||||
receipt_files = [f for f in all_files if receipt_pattern in f]
|
||||
|
||||
return render_template('list.html', list=shopping_list, items=items, receipt_files=receipt_files)
|
||||
return render_template(
|
||||
'list.html',
|
||||
list=shopping_list,
|
||||
items=items,
|
||||
receipt_files=receipt_files,
|
||||
total_count=total_count,
|
||||
purchased_count=purchased_count,
|
||||
percent=percent
|
||||
)
|
||||
|
||||
|
||||
@app.route('/share/<token>')
|
||||
def share_list(token):
|
||||
@ -297,11 +320,56 @@ def update_note(item_id):
|
||||
def admin_panel():
|
||||
if not current_user.is_admin:
|
||||
return redirect(url_for('index_guest'))
|
||||
|
||||
user_count = User.query.count()
|
||||
list_count = ShoppingList.query.count()
|
||||
item_count = Item.query.count()
|
||||
all_lists = ShoppingList.query.options(db.joinedload(ShoppingList.owner)).all()
|
||||
return render_template('admin/admin_panel.html', user_count=user_count, list_count=list_count, item_count=item_count, all_lists=all_lists)
|
||||
|
||||
# Pobierz folder uploadów
|
||||
all_files = os.listdir(app.config['UPLOAD_FOLDER'])
|
||||
|
||||
enriched_lists = []
|
||||
for l in all_lists:
|
||||
items = Item.query.filter_by(list_id=l.id).all()
|
||||
total_count = len(items)
|
||||
purchased_count = len([i for i in items if i.purchased])
|
||||
percent = (purchased_count / total_count * 100) if total_count > 0 else 0
|
||||
comments_count = len([i for i in items if i.note and i.note.strip() != ''])
|
||||
purchased_items_count = Item.query.filter_by(purchased=True).count()
|
||||
|
||||
|
||||
receipt_pattern = f"list_{l.id}"
|
||||
receipt_files = [f for f in all_files if receipt_pattern in f]
|
||||
|
||||
enriched_lists.append({
|
||||
'list': l,
|
||||
'total_count': total_count,
|
||||
'purchased_count': purchased_count,
|
||||
'percent': round(percent),
|
||||
'comments_count': comments_count,
|
||||
'receipts_count': len(receipt_files)
|
||||
})
|
||||
|
||||
|
||||
top_products = (
|
||||
db.session.query(Item.name, func.count(Item.id).label('count'))
|
||||
.filter(Item.purchased == True)
|
||||
.group_by(Item.name)
|
||||
.order_by(func.count(Item.id).desc())
|
||||
.limit(5)
|
||||
.all()
|
||||
)
|
||||
|
||||
return render_template(
|
||||
'admin/admin_panel.html',
|
||||
user_count=user_count,
|
||||
list_count=list_count,
|
||||
item_count=item_count,
|
||||
purchased_items_count=purchased_items_count,
|
||||
enriched_lists=enriched_lists,
|
||||
top_products=top_products,
|
||||
)
|
||||
|
||||
@app.route('/admin/delete_list/<int:list_id>')
|
||||
@login_required
|
||||
@ -392,7 +460,6 @@ def admin_receipts():
|
||||
upload_folder=app.config['UPLOAD_FOLDER']
|
||||
)
|
||||
|
||||
|
||||
@app.route('/admin/delete_receipt/<filename>')
|
||||
@login_required
|
||||
def delete_receipt(filename):
|
||||
@ -406,6 +473,56 @@ def delete_receipt(filename):
|
||||
flash('Plik nie istnieje', 'danger')
|
||||
return redirect(url_for('admin_receipts'))
|
||||
|
||||
@app.route('/admin/edit_list/<int:list_id>', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def edit_list(list_id):
|
||||
if not current_user.is_admin:
|
||||
return redirect(url_for('index_guest'))
|
||||
l = ShoppingList.query.get_or_404(list_id)
|
||||
if request.method == 'POST':
|
||||
new_title = request.form.get('title')
|
||||
if new_title:
|
||||
l.title = new_title
|
||||
db.session.commit()
|
||||
flash('Zaktualizowano tytuł listy', 'success')
|
||||
return redirect(url_for('admin_panel'))
|
||||
return render_template('admin/edit_list.html', list=l)
|
||||
|
||||
@app.route('/admin/delete_selected_lists', methods=['POST'])
|
||||
@login_required
|
||||
def delete_selected_lists():
|
||||
if not current_user.is_admin:
|
||||
return redirect(url_for('index_guest'))
|
||||
ids = request.form.getlist('list_ids')
|
||||
for list_id in ids:
|
||||
lst = ShoppingList.query.get(int(list_id))
|
||||
if lst:
|
||||
Item.query.filter_by(list_id=lst.id).delete()
|
||||
db.session.delete(lst)
|
||||
db.session.commit()
|
||||
flash('Usunięto wybrane listy', 'success')
|
||||
return redirect(url_for('admin_panel'))
|
||||
|
||||
@app.route('/admin/archive_list/<int:list_id>')
|
||||
@login_required
|
||||
def archive_list(list_id):
|
||||
if not current_user.is_admin:
|
||||
return redirect(url_for('index_guest'))
|
||||
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
|
||||
def delete_all_items():
|
||||
if not current_user.is_admin:
|
||||
return redirect(url_for('index_guest'))
|
||||
Item.query.delete()
|
||||
db.session.commit()
|
||||
flash('Usunięto wszystkie produkty', 'success')
|
||||
return redirect(url_for('admin_panel'))
|
||||
|
||||
@socketio.on('delete_item')
|
||||
def handle_delete_item(data):
|
||||
@ -460,7 +577,16 @@ def handle_check_item(data):
|
||||
item.purchased = True
|
||||
item.purchased_at = datetime.utcnow()
|
||||
db.session.commit()
|
||||
|
||||
purchased_count, total_count, percent = get_progress(item.list_id)
|
||||
|
||||
emit('item_checked', {'item_id': item.id}, to=str(item.list_id))
|
||||
emit('progress_updated', {
|
||||
'purchased_count': purchased_count,
|
||||
'total_count': total_count,
|
||||
'percent': percent
|
||||
}, to=str(item.list_id))
|
||||
|
||||
|
||||
@socketio.on('uncheck_item')
|
||||
def handle_uncheck_item(data):
|
||||
@ -469,7 +595,15 @@ def handle_uncheck_item(data):
|
||||
item.purchased = False
|
||||
item.purchased_at = None
|
||||
db.session.commit()
|
||||
|
||||
purchased_count, total_count, percent = get_progress(item.list_id)
|
||||
|
||||
emit('item_unchecked', {'item_id': item.id}, to=str(item.list_id))
|
||||
emit('progress_updated', {
|
||||
'purchased_count': purchased_count,
|
||||
'total_count': total_count,
|
||||
'percent': percent
|
||||
}, to=str(item.list_id))
|
||||
|
||||
@socketio.on('update_note')
|
||||
def handle_update_note(data):
|
||||
@ -481,6 +615,8 @@ def handle_update_note(data):
|
||||
db.session.commit()
|
||||
emit('note_updated', {'item_id': item_id, 'note': note}, to=str(item.list_id))
|
||||
|
||||
|
||||
|
||||
@app.cli.command('create_db')
|
||||
def create_db():
|
||||
db.create_all()
|
||||
|
@ -119,6 +119,22 @@ function setupList(listId, username) {
|
||||
updateProgressBar();
|
||||
});
|
||||
|
||||
|
||||
socket.on('progress_updated', function(data) {
|
||||
const progressBar = document.getElementById('progress-bar');
|
||||
if (progressBar) {
|
||||
progressBar.style.width = data.percent + '%';
|
||||
progressBar.setAttribute('aria-valuenow', data.percent);
|
||||
progressBar.textContent = Math.round(data.percent) + '%';
|
||||
}
|
||||
|
||||
const progressTitle = document.getElementById('progress-title');
|
||||
if (progressTitle) {
|
||||
progressTitle.textContent = `📊 Postęp listy — ${data.purchased_count}/${data.total_count} kupionych (${Math.round(data.percent)}%)`;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
socket.on('note_updated', data => {
|
||||
const itemEl = document.getElementById(`item-${data.item_id}`);
|
||||
if (itemEl) {
|
||||
|
@ -7,53 +7,93 @@
|
||||
<a href="/" class="btn btn-outline-secondary">← Powrót do strony głównej</a>
|
||||
</div>
|
||||
|
||||
<div class="d-flex flex-wrap gap-2 mb-4">
|
||||
<a href="/admin/add_user" class="btn btn-success">➕ Dodaj użytkownika</a>
|
||||
<a href="/admin/users" class="btn btn-outline-light">👥 Lista użytkowników</a>
|
||||
<a href="/admin/receipts" class="btn btn-outline-light">📸 Wszystkie paragony</a>
|
||||
<a href="/admin/delete_all_lists" class="btn btn-danger">🗑️ Usuń wszystkie listy</a>
|
||||
<a href="/admin/delete_all_items" class="btn btn-danger">❌ Usuń wszystkie produkty</a>
|
||||
</div>
|
||||
|
||||
<div class="card bg-dark text-white mb-4">
|
||||
<div class="card-body">
|
||||
<p><strong>👤 Liczba użytkowników:</strong> {{ user_count }}</p>
|
||||
<p><strong>📝 Liczba list:</strong> {{ list_count }}</p>
|
||||
<p><strong>📝 Liczba list zakupowych:</strong> {{ list_count }}</p>
|
||||
<p><strong>🛒 Liczba produktów:</strong> {{ item_count }}</p>
|
||||
<p><strong>✅ Zakupionych produktów:</strong> {{ purchased_items_count }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex flex-wrap gap-2 mb-4">
|
||||
<a href="/admin/users" class="btn btn-outline-light">👥 Lista użytkowników</a>
|
||||
<a href="/admin/add_user" class="btn btn-success">➕ Dodaj użytkownika</a>
|
||||
<a href="/admin/receipts" class="btn btn-outline-light">📸 Wszystkie paragony</a>
|
||||
<a href="/admin/delete_all_lists" class="btn btn-danger">🗑️ Usuń wszystkie listy</a>
|
||||
{% if top_products %}
|
||||
<div class="card bg-dark text-white mb-4">
|
||||
<div class="card-body">
|
||||
<h5>🔥 Najczęściej kupowane produkty:</h5>
|
||||
<ul class="mb-0">
|
||||
{% for name, count in top_products %}
|
||||
<li>{{ name }} — {{ count }}×</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<h3 class="mt-4">📄 Wszystkie listy zakupowe</h3>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-dark table-striped align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Tytuł</th>
|
||||
<th>Utworzono</th>
|
||||
<th>Właściciel (ID / nazwa)</th>
|
||||
<th>Akcje</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for l in all_lists %}
|
||||
<tr>
|
||||
<td>{{ l.id }}</td>
|
||||
<td class="fw-bold">{{ l.title }}</td>
|
||||
<td>{{ l.created_at.strftime('%Y-%m-%d %H:%M') if l.created_at else '-' }}</td>
|
||||
<td>
|
||||
{% if l.owner_id %}
|
||||
{{ l.owner_id }} / {{ l.owner.username if l.owner else 'Brak użytkownika' }}
|
||||
{% else %}
|
||||
-
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<a href="{{ url_for('delete_list', list_id=l.id) }}" class="btn btn-sm btn-outline-danger">🗑️ Usuń</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<form method="post" action="{{ url_for('delete_selected_lists') }}">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-dark table-striped align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><input type="checkbox" id="select-all"></th>
|
||||
<th>ID</th>
|
||||
<th>Tytuł</th>
|
||||
<th>Utworzono</th>
|
||||
<th>Właściciel</th>
|
||||
<th>Produkty</th>
|
||||
<th>Wypełnienie</th>
|
||||
<th>Komentarze</th>
|
||||
<th>Paragony</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">
|
||||
<a href="{{ url_for('view_list', list_id=l.id) }}" class="text-white">{{ l.title }}</a>
|
||||
</td>
|
||||
<td>{{ l.created_at.strftime('%Y-%m-%d %H:%M') if l.created_at else '-' }}</td>
|
||||
<td>
|
||||
{% if l.owner_id %}
|
||||
{{ l.owner_id }} / {{ l.owner.username if l.owner else 'Brak użytkownika' }}
|
||||
{% else %}
|
||||
-
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ e.total_count }}</td>
|
||||
<td>{{ e.purchased_count }}/{{ e.total_count }} ({{ e.percent }}%)</td>
|
||||
<td>{{ e.comments_count }}</td>
|
||||
<td>{{ e.receipts_count }}</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>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-danger mt-2">🗑️ Usuń zaznaczone listy</button>
|
||||
</form>
|
||||
|
||||
<script>
|
||||
document.getElementById('select-all').addEventListener('click', function(){
|
||||
const checkboxes = document.querySelectorAll('input[name="list_ids"]');
|
||||
checkboxes.forEach(cb => cb.checked = this.checked);
|
||||
});
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
||||
|
18
templates/admin/edit_list.html
Normal file
18
templates/admin/edit_list.html
Normal file
@ -0,0 +1,18 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block title %}Edytuj listę{% endblock %}
|
||||
{% block content %}
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center flex-wrap mb-4">
|
||||
<h2 class="mb-2">✏️ Edytuj tytuł listy</h2>
|
||||
<a href="{{ url_for('admin_panel') }}" class="btn btn-outline-secondary">← Powrót</a>
|
||||
</div>
|
||||
|
||||
<form method="post">
|
||||
<div class="mb-3">
|
||||
<label for="title" class="form-label">Nowy tytuł</label>
|
||||
<input type="text" class="form-control" id="title" name="title" value="{{ list.title }}" required>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-success">💾 Zapisz</button>
|
||||
</form>
|
||||
|
||||
{% endblock %}
|
@ -14,8 +14,18 @@
|
||||
<body class="bg-dark text-white">
|
||||
|
||||
<nav class="navbar navbar-dark bg-dark mb-3">
|
||||
<div class="container-fluid d-flex justify-content-between">
|
||||
<a class="navbar-brand" href="/">Live Lista Zakupów</a>
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand fw-bold fs-4 text-success" href="/">
|
||||
🛒 Live <span class="text-warning">Lista</span> Zakupów
|
||||
</a>
|
||||
|
||||
|
||||
{% if current_user.is_authenticated %}
|
||||
<span class="mx-auto text-white">Zalogowany jako: <strong>{{ current_user.username }}</strong></span>
|
||||
{% else %}
|
||||
<span class="mx-auto text-white">Przeglądasz jako <strong>gość</strong></span>
|
||||
{% endif %}
|
||||
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
{% if current_user.is_authenticated and current_user.is_admin %}
|
||||
<a href="{{ url_for('admin_panel') }}" class="btn btn-outline-warning btn-sm">⚙️ Panel admina</a>
|
||||
@ -28,6 +38,8 @@
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
|
||||
<div id="toast-container" class="toast-container position-fixed bottom-0 end-0 p-3"></div>
|
||||
|
||||
<div class="container px-2">
|
||||
|
@ -3,26 +3,28 @@
|
||||
{% block content %}
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center flex-wrap mb-4">
|
||||
<h2 class="mb-2">Twoje listy zakupów</h2>
|
||||
<a href="{{ url_for('index_guest') }}" class="btn btn-outline-secondary">← Powrót do panelu</a>
|
||||
<h2 class="mb-2">Stwórz nową listę</h2>
|
||||
</div>
|
||||
|
||||
<div class="card bg-dark text-white mb-4">
|
||||
<div class="card-body">
|
||||
<form action="/create" method="post">
|
||||
<div class="mb-3">
|
||||
<label for="title" class="form-label">Nazwa listy</label>
|
||||
<input type="text" name="title" id="title" placeholder="Wprowadź nazwę listy" required class="form-control">
|
||||
</div>
|
||||
<div class="form-check mb-3">
|
||||
<input type="checkbox" name="temporary" class="form-check-input" id="tempCheck">
|
||||
<label for="tempCheck" class="form-check-label">Tymczasowa (7 dni)</label>
|
||||
<div class="input-group mb-3">
|
||||
<input type="text" name="title" id="title" placeholder="Wprowadź nazwę nowej listy" required class="form-control">
|
||||
<div class="input-group-text">
|
||||
<input type="checkbox" name="temporary" class="form-check-input m-0" id="tempCheck">
|
||||
<label for="tempCheck" class="ms-2 mb-0">Tymczasowa (7 dni)</label>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-success w-100">➕ Utwórz nową listę</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center flex-wrap mb-4">
|
||||
<h2 class="mb-2">Listy zakupów</h2>
|
||||
</div>
|
||||
|
||||
{% if lists %}
|
||||
<ul class="list-group">
|
||||
{% for l in lists %}
|
||||
@ -31,7 +33,7 @@
|
||||
{% set percent = (purchased_count / total_count * 100) if total_count > 0 else 0 %}
|
||||
<li class="list-group-item bg-dark text-white">
|
||||
<div class="d-flex justify-content-between align-items-center flex-wrap w-100">
|
||||
<span class="fw-bold">{{ l.title }}</span>
|
||||
<span class="fw-bold">{{ l.title }} (Autor: {{ l.owner.username }})</span>
|
||||
<div class="mt-2 mt-md-0">
|
||||
{% if current_user.is_authenticated %}
|
||||
<a href="/list/{{ l.id }}" class="btn btn-sm btn-outline-light me-1">📄 Otwórz</a>
|
||||
@ -42,7 +44,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="progress mt-2" style="height: 20px;">
|
||||
<div class="progress-bar bg-success" role="progressbar" style="width: {{ percent }}%" aria-valuenow="{{ percent }}" aria-valuemin="0" aria-valuemax="100">{{ purchased_count }}/{{ total_count }} ({{ percent|round(0) }}%)</div>
|
||||
<div class="progress-bar bg-warning" role="progressbar" style="width: {{ percent }}%" aria-valuenow="{{ percent }}" aria-valuemin="0" aria-valuemax="100">Produkty: {{ purchased_count }}/{{ total_count }} ({{ percent|round(0) }}%)</div>
|
||||
</div>
|
||||
</li>
|
||||
{% endfor %}
|
||||
|
@ -3,27 +3,37 @@
|
||||
{% block content %}
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mb-3 flex-wrap">
|
||||
<h2 class="mb-2">{{ list.title }}</h2>
|
||||
<h2 class="mb-2">Lista: <strong>{{ list.title }}</strong></h2>
|
||||
<a href="/" class="btn btn-outline-secondary">← Powrót do list</a>
|
||||
</div>
|
||||
|
||||
<div class="card bg-dark text-white mb-4">
|
||||
<div class="card-body d-flex flex-wrap justify-content-between align-items-center">
|
||||
<div class="mb-2 mb-md-0">
|
||||
<strong>Udostępnij link:</strong>
|
||||
<span class="badge bg-secondary">{{ request.url_root }}share/{{ list.share_token }}</span>
|
||||
<div class="card-body d-flex flex-column flex-md-row justify-content-between align-items-start align-items-md-center gap-2">
|
||||
<div>
|
||||
<strong>🔗 Udostępnij link:</strong><br>
|
||||
<span class="badge bg-secondary text-wrap" style="font-size: 0.9rem;">
|
||||
{{ request.url_root }}share/{{ list.share_token }}
|
||||
</span>
|
||||
</div>
|
||||
<button class="btn btn-outline-light btn-sm" onclick="copyLink('{{ request.url_root }}share/{{ list.share_token }}')">
|
||||
📋 Skopiuj lub udostępnij
|
||||
<button class="btn btn-success btn-sm mt-2 mt-md-0" onclick="copyLink('{{ request.url_root }}share/{{ list.share_token }}')">
|
||||
📋 udostępnij link
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Progress bar (dynamic) -->
|
||||
<div class="progress mb-3">
|
||||
<div id="progress-bar" class="progress-bar bg-success" role="progressbar" style="width: 0%" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100">0%</div>
|
||||
<h5 id="progress-title" class="mb-2">
|
||||
📊 Postęp listy — {{ purchased_count }}/{{ total_count }} kupionych ({{ percent|round(0) }}%)
|
||||
</h5>
|
||||
<div class="progress mb-3" style="height: 25px;">
|
||||
<div id="progress-bar" class="progress-bar bg-warning" role="progressbar"
|
||||
style="width: {{ percent }}%;" aria-valuenow="{{ percent }}"
|
||||
aria-valuemin="0" aria-valuemax="100">
|
||||
{{ percent|round(0) }}%
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<ul id="items" class="list-group mb-3">
|
||||
{% for item in items %}
|
||||
<li class="list-group-item d-flex justify-content-between align-items-center flex-wrap {% if item.purchased %}bg-success text-white{% else %}bg-light text-white{% endif %}" id="item-{{ item.id }}">
|
||||
|
@ -4,7 +4,6 @@
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center flex-wrap mb-3">
|
||||
<h2 class="mb-2">🛍️ {{ list.title }} <small class="text-muted">(Gość)</small></h2>
|
||||
<a href="/" class="btn btn-outline-secondary btn-sm">← Powrót</a>
|
||||
</div>
|
||||
|
||||
<ul id="items" class="list-group mb-3">
|
||||
|
Reference in New Issue
Block a user