duzo zmian, funkcji
This commit is contained in:
11
alters.txt
11
alters.txt
@ -10,3 +10,14 @@ ADD COLUMN note TEXT;
|
||||
|
||||
# NOWE FUNKCJE ADMINA
|
||||
ALTER TABLE shopping_list ADD COLUMN is_archived BOOLEAN DEFAULT FALSE;
|
||||
|
||||
|
||||
# FUNKCJA WYDATKOW
|
||||
CREATE TABLE expense (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
list_id INTEGER,
|
||||
amount FLOAT NOT NULL,
|
||||
added_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
receipt_filename VARCHAR(255),
|
||||
FOREIGN KEY(list_id) REFERENCES shopping_list(id)
|
||||
);
|
183
app.py
183
app.py
@ -12,7 +12,7 @@ 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
|
||||
from sqlalchemy import func, extract
|
||||
|
||||
app = Flask(__name__)
|
||||
app.config.from_object(Config)
|
||||
@ -64,6 +64,13 @@ class SuggestedProduct(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
name = db.Column(db.String(150), unique=True, nullable=False)
|
||||
|
||||
class Expense(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
list_id = db.Column(db.Integer, db.ForeignKey('shopping_list.id'))
|
||||
amount = db.Column(db.Float, nullable=False)
|
||||
added_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
receipt_filename = db.Column(db.String(255), nullable=True)
|
||||
|
||||
@static_bp.route('/static/js/live.js')
|
||||
def serve_live_js():
|
||||
response = send_from_directory('static/js', 'live.js')
|
||||
@ -161,11 +168,20 @@ def system_auth():
|
||||
|
||||
@app.route('/')
|
||||
def index_guest():
|
||||
lists = ShoppingList.query.all()
|
||||
now = datetime.utcnow()
|
||||
lists = ShoppingList.query.filter(
|
||||
((ShoppingList.expires_at == None) | (ShoppingList.expires_at > now)),
|
||||
ShoppingList.is_archived == False
|
||||
).order_by(ShoppingList.created_at.desc()).all()
|
||||
|
||||
for l in lists:
|
||||
items = Item.query.filter_by(list_id=l.id).all()
|
||||
l.total_count = len(items)
|
||||
l.purchased_count = len([i for i in items if i.purchased])
|
||||
|
||||
expenses = Expense.query.filter_by(list_id=l.id).all()
|
||||
l.total_expense = sum(e.amount for e in expenses)
|
||||
|
||||
return render_template('index.html', lists=lists)
|
||||
|
||||
@app.errorhandler(404)
|
||||
@ -217,11 +233,11 @@ def create_list():
|
||||
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
|
||||
|
||||
expenses = Expense.query.filter_by(list_id=list_id).all()
|
||||
total_expense = sum(e.amount for e in expenses)
|
||||
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]
|
||||
@ -233,10 +249,11 @@ def view_list(list_id):
|
||||
receipt_files=receipt_files,
|
||||
total_count=total_count,
|
||||
purchased_count=purchased_count,
|
||||
percent=percent
|
||||
percent=percent,
|
||||
expenses=expenses,
|
||||
total_expense=total_expense
|
||||
)
|
||||
|
||||
|
||||
@app.route('/share/<token>')
|
||||
def share_list(token):
|
||||
shopping_list = ShoppingList.query.filter_by(share_token=token).first_or_404()
|
||||
@ -246,21 +263,36 @@ def share_list(token):
|
||||
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_guest.html', list=shopping_list, items=items, receipt_files=receipt_files)
|
||||
expenses = Expense.query.filter_by(list_id=shopping_list.id).all()
|
||||
total_expense = sum(e.amount for e in expenses)
|
||||
|
||||
return render_template(
|
||||
'list_guest.html',
|
||||
list=shopping_list,
|
||||
items=items,
|
||||
receipt_files=receipt_files,
|
||||
expenses=expenses,
|
||||
total_expense=total_expense
|
||||
)
|
||||
|
||||
|
||||
@app.route('/guest-list/<int:list_id>')
|
||||
def guest_list(list_id):
|
||||
shopping_list = ShoppingList.query.get_or_404(list_id)
|
||||
items = Item.query.filter_by(list_id=list_id).all()
|
||||
|
||||
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_guest.html', list=shopping_list, items=items, receipt_files=receipt_files)
|
||||
|
||||
expenses = Expense.query.filter_by(list_id=list_id).all()
|
||||
total_expense = sum(e.amount for e in expenses)
|
||||
return render_template(
|
||||
'list_guest.html',
|
||||
list=shopping_list,
|
||||
items=items,
|
||||
receipt_files=receipt_files,
|
||||
expenses=expenses,
|
||||
total_expense=total_expense
|
||||
)
|
||||
|
||||
@app.route('/copy/<int:list_id>')
|
||||
@login_required
|
||||
@ -349,9 +381,8 @@ def admin_panel():
|
||||
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()
|
||||
|
||||
|
||||
expenses = Expense.query.filter_by(list_id=l.id).all()
|
||||
total_expense = sum(e.amount for e in expenses)
|
||||
receipt_pattern = f"list_{l.id}"
|
||||
receipt_files = [f for f in all_files if receipt_pattern in f]
|
||||
|
||||
@ -361,28 +392,53 @@ def admin_panel():
|
||||
'purchased_count': purchased_count,
|
||||
'percent': round(percent),
|
||||
'comments_count': comments_count,
|
||||
'receipts_count': len(receipt_files)
|
||||
'receipts_count': len(receipt_files),
|
||||
'total_expense': total_expense
|
||||
})
|
||||
|
||||
# Najczęściej kupowane produkty
|
||||
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()
|
||||
)
|
||||
|
||||
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()
|
||||
)
|
||||
purchased_items_count = Item.query.filter_by(purchased=True).count()
|
||||
|
||||
# Podsumowanie wydatków
|
||||
total_expense_sum = db.session.query(func.sum(Expense.amount)).scalar() or 0
|
||||
|
||||
current_year = datetime.utcnow().year
|
||||
year_expense_sum = (
|
||||
db.session.query(func.sum(Expense.amount))
|
||||
.filter(extract('year', Expense.added_at) == current_year)
|
||||
.scalar() or 0
|
||||
)
|
||||
|
||||
current_month = datetime.utcnow().month
|
||||
month_expense_sum = (
|
||||
db.session.query(func.sum(Expense.amount))
|
||||
.filter(extract('year', Expense.added_at) == current_year)
|
||||
.filter(extract('month', Expense.added_at) == current_month)
|
||||
.scalar() or 0
|
||||
)
|
||||
|
||||
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,
|
||||
total_expense_sum=total_expense_sum,
|
||||
year_expense_sum=year_expense_sum,
|
||||
month_expense_sum=month_expense_sum,
|
||||
)
|
||||
|
||||
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
|
||||
@ -491,15 +547,46 @@ def delete_receipt(filename):
|
||||
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)
|
||||
expenses = Expense.query.filter_by(list_id=list_id).all()
|
||||
total_expense = sum(e.amount for e in expenses)
|
||||
|
||||
if request.method == 'POST':
|
||||
new_title = request.form.get('title')
|
||||
if new_title:
|
||||
l.title = new_title
|
||||
new_amount_str = request.form.get('amount')
|
||||
is_archived = 'archived' in request.form
|
||||
|
||||
if new_title and new_title.strip():
|
||||
l.title = new_title.strip()
|
||||
|
||||
l.is_archived = is_archived
|
||||
|
||||
if new_amount_str:
|
||||
try:
|
||||
new_amount = float(new_amount_str)
|
||||
|
||||
|
||||
if expenses:
|
||||
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()
|
||||
flash('Zaktualizowano tytuł, 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ł listy', 'success')
|
||||
flash('Zaktualizowano tytuł i/lub archiwizację', 'success')
|
||||
|
||||
return redirect(url_for('admin_panel'))
|
||||
return render_template('admin/edit_list.html', list=l)
|
||||
|
||||
return render_template('admin/edit_list.html', list=l, total_expense=total_expense)
|
||||
|
||||
|
||||
@app.route('/admin/delete_selected_lists', methods=['POST'])
|
||||
@login_required
|
||||
@ -537,6 +624,11 @@ def delete_all_items():
|
||||
flash('Usunięto wszystkie produkty', 'success')
|
||||
return redirect(url_for('admin_panel'))
|
||||
|
||||
|
||||
# =========================================================================================
|
||||
# SOCKET.IO
|
||||
# =========================================================================================
|
||||
|
||||
@socketio.on('delete_item')
|
||||
def handle_delete_item(data):
|
||||
item = Item.query.get(data['item_id'])
|
||||
@ -627,6 +719,23 @@ def handle_update_note(data):
|
||||
db.session.commit()
|
||||
emit('note_updated', {'item_id': item_id, 'note': note}, to=str(item.list_id))
|
||||
|
||||
@socketio.on('add_expense')
|
||||
def handle_add_expense(data):
|
||||
list_id = data['list_id']
|
||||
amount = data['amount']
|
||||
|
||||
new_expense = Expense(list_id=list_id, amount=amount)
|
||||
db.session.add(new_expense)
|
||||
db.session.commit()
|
||||
|
||||
total = db.session.query(func.sum(Expense.amount)).filter_by(list_id=list_id).scalar() or 0
|
||||
|
||||
emit('expense_added', {
|
||||
'amount': amount,
|
||||
'total': total
|
||||
}, to=str(list_id))
|
||||
|
||||
|
||||
@app.cli.command('create_db')
|
||||
def create_db():
|
||||
db.create_all()
|
||||
|
@ -91,6 +91,24 @@ function setupList(listId, username) {
|
||||
updateItemState(data.item_id, false);
|
||||
});
|
||||
|
||||
socket.on('expense_added', data => {
|
||||
// Osobne bo w html musi byc unikatowy a wyswietlam w dwoch miejscach
|
||||
const badgeEl = document.getElementById('total-expense1');
|
||||
if (badgeEl) {
|
||||
badgeEl.innerHTML = `💸 ${data.total.toFixed(2)} PLN`;
|
||||
badgeEl.classList.remove('bg-secondary');
|
||||
badgeEl.classList.add('bg-success');
|
||||
badgeEl.style.display = '';
|
||||
}
|
||||
|
||||
const summaryEl = document.getElementById('total-expense2');
|
||||
if (summaryEl) {
|
||||
summaryEl.innerHTML = `<b>💸 Łącznie wydano:</b> ${data.total.toFixed(2)} PLN`;
|
||||
}
|
||||
|
||||
showToast(`Dodano wydatek: ${data.amount.toFixed(2)} PLN`);
|
||||
});
|
||||
|
||||
socket.on('item_added', data => {
|
||||
showToast(`${data.added_by} dodał: ${data.name}`);
|
||||
const li = document.createElement('li');
|
||||
@ -226,6 +244,21 @@ function editItem(id, oldName) {
|
||||
}
|
||||
}
|
||||
|
||||
function submitExpense() {
|
||||
const amountInput = document.getElementById('expenseAmount');
|
||||
const amount = parseFloat(amountInput.value);
|
||||
if (isNaN(amount) || amount <= 0) {
|
||||
showToast('Podaj poprawną kwotę!');
|
||||
return;
|
||||
}
|
||||
socket.emit('add_expense', {
|
||||
list_id: LIST_ID,
|
||||
amount: amount
|
||||
});
|
||||
amountInput.value = '';
|
||||
}
|
||||
|
||||
|
||||
function copyLink(link) {
|
||||
if (navigator.share) {
|
||||
navigator.share({
|
||||
|
@ -7,35 +7,81 @@
|
||||
<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>
|
||||
<nav class="navbar navbar-expand-lg navbar-dark bg-dark rounded mb-4">
|
||||
<div class="container-fluid p-0">
|
||||
<a class="navbar-brand" href="#">Admin</a>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#adminNavbar" aria-controls="adminNavbar" aria-expanded="false" aria-label="Przełącz nawigację">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
|
||||
<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 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 class="collapse navbar-collapse" id="adminNavbar">
|
||||
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/admin/add_user">➕ Dodaj użytkownika</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/admin/users">👥 Lista użytkowników</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/admin/receipts">📸 Wszystkie paragony</a>
|
||||
</li>
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link dropdown-toggle text-danger" href="#" id="clearDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
🗑️ Czyszczenie
|
||||
</a>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a class="dropdown-item text-danger" href="/admin/delete_all_lists">Usuń wszystkie listy</a></li>
|
||||
<li><a class="dropdown-item text-danger" href="/admin/delete_all_items">Usuń wszystkie produkty</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-md-4">
|
||||
<div class="card bg-dark text-white h-100">
|
||||
<div class="card-body">
|
||||
<p><strong>👤 Liczba użytkowników:</strong> {{ user_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>
|
||||
|
||||
{% if top_products %}
|
||||
<div class="col-md-4">
|
||||
<div class="card bg-dark text-white h-100">
|
||||
<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>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="col-md-4">
|
||||
<div class="card bg-dark text-white h-100">
|
||||
<div class="card-body">
|
||||
<h5>💸 Podsumowanie wydatków:</h5>
|
||||
<ul class="mb-0">
|
||||
<li><strong>Obecny miesiąc:</strong> {{ '%.2f'|format(month_expense_sum) }} PLN</li>
|
||||
<li><strong>Obecny rok:</strong> {{ '%.2f'|format(year_expense_sum) }} PLN</li>
|
||||
<li><strong>Całkowite:</strong> {{ '%.2f'|format(total_expense_sum) }} PLN</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% 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>
|
||||
<form method="post" action="{{ url_for('delete_selected_lists') }}">
|
||||
@ -52,6 +98,7 @@
|
||||
<th>Wypełnienie</th>
|
||||
<th>Komentarze</th>
|
||||
<th>Paragony</th>
|
||||
<th>Wydatki</th>
|
||||
<th>Akcje</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@ -76,14 +123,22 @@
|
||||
<td>{{ e.purchased_count }}/{{ e.total_count }} ({{ e.percent }}%)</td>
|
||||
<td>{{ e.comments_count }}</td>
|
||||
<td>{{ e.receipts_count }}</td>
|
||||
<td>
|
||||
{% if e.total_expense > 0 %}
|
||||
{{ '%.2f'|format(e.total_expense) }} PLN
|
||||
{% else %}
|
||||
-
|
||||
{% endif %}
|
||||
</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('edit_list', list_id=l.id) }}" class="btn btn-sm btn-outline-primary">✏️ Edytuj liste</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>
|
||||
|
@ -3,7 +3,7 @@
|
||||
{% block content %}
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center flex-wrap mb-4">
|
||||
<h2 class="mb-2">✏️ Edytuj tytuł listy</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>
|
||||
|
||||
@ -12,7 +12,22 @@
|
||||
<label for="title" class="form-label">Nowy tytuł</label>
|
||||
<input type="text" class="form-control" id="title" name="title" value="{{ list.title }}" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="amount" class="form-label">Nowa kwota wydatku (PLN)</label>
|
||||
<input type="number" step="0.01" min="0" class="form-control" id="amount" name="amount" value="{{ '%.2f'|format(total_expense) }}">
|
||||
<small class="form-text text-muted">Jeśli nie chcesz zmieniać kwoty, zostaw to pole bez zmian.</small>
|
||||
</div>
|
||||
|
||||
<div class="form-check 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>
|
||||
|
||||
<button type="submit" class="btn btn-success">💾 Zapisz</button>
|
||||
<a href="{{ url_for('admin_panel') }}" class="btn btn-secondary">Anuluj</a>
|
||||
</form>
|
||||
|
||||
{% endblock %}
|
||||
|
@ -44,7 +44,12 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="progress mt-2" style="height: 20px;">
|
||||
<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 class="progress-bar bg-warning text-dark fw-bold" role="progressbar" style="width: {{ percent }}%" aria-valuenow="{{ percent }}" aria-valuemin="0" aria-valuemax="100">
|
||||
Produkty: {{ purchased_count }}/{{ total_count }} ({{ percent|round(0) }}%)
|
||||
{% if l.total_expense > 0 %}
|
||||
— 💸 {{ '%.2f'|format(l.total_expense) }} PLN
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
{% endfor %}
|
||||
|
@ -3,7 +3,16 @@
|
||||
{% block content %}
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mb-3 flex-wrap">
|
||||
<h2 class="mb-2">Lista: <strong>{{ list.title }}</strong></h2>
|
||||
|
||||
<h2 class="mb-2">
|
||||
Lista: <strong>{{ list.title }}</strong>
|
||||
{% if list.is_archived %}
|
||||
<span class="badge bg-secondary ms-2">(Archiwalna)</span>
|
||||
{% endif %}
|
||||
</h2>
|
||||
|
||||
|
||||
|
||||
<a href="/" class="btn btn-outline-secondary">← Powrót do list</a>
|
||||
</div>
|
||||
|
||||
@ -15,9 +24,15 @@
|
||||
{{ request.url_root }}share/{{ list.share_token }}
|
||||
</span>
|
||||
</div>
|
||||
<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>
|
||||
{% if not list.is_archived %}
|
||||
<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>
|
||||
{% else %}
|
||||
<button class="btn btn-secondary btn-sm mt-2 mt-md-0" disabled>
|
||||
📋 Udostępnianie wyłączone (Archiwalna)
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -25,7 +40,8 @@
|
||||
<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 class="progress mb-2" 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">
|
||||
@ -33,30 +49,49 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if total_expense > 0 %}
|
||||
<div id="total-expense2" class="text-success fw-bold mb-3">
|
||||
💸 Łącznie wydano: {{ '%.2f'|format(total_expense) }} PLN
|
||||
</div>
|
||||
{% else %}
|
||||
<div id="total-expense2" class="text-success fw-bold mb-3" style="display: none;">
|
||||
💸 Łącznie wydano: 0.00 PLN
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
||||
<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 }}">
|
||||
<div class="d-flex align-items-center flex-wrap gap-2 flex-grow-1">
|
||||
<input type="checkbox" {% if item.purchased %}checked{% endif %}>
|
||||
<input type="checkbox" {% if item.purchased %}checked{% endif %} {% if list.is_archived %}disabled{% endif %}>
|
||||
<span id="name-{{ item.id }}" class="{% if item.purchased %}text-white{% else %}text-white{% endif %}">{{ item.name }}</span>
|
||||
{% if item.note %}
|
||||
<small class="text-danger">[ Notatka: <b>{{ item.note }}</b> ] </small>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="mt-2 mt-md-0 d-flex gap-1">
|
||||
<button class="btn btn-sm btn-outline-warning" onclick="editItem({{ item.id }}, '{{ item.name }}')">✏️ Edytuj</button>
|
||||
<button class="btn btn-sm btn-outline-danger" onclick="deleteItem({{ item.id }})">🗑️ Usuń</button>
|
||||
<button class="btn btn-sm btn-outline-warning"
|
||||
{% if list.is_archived %}disabled{% else %}onclick="editItem({{ item.id }}, '{{ item.name }}')"{% endif %}>
|
||||
✏️ Edytuj
|
||||
</button>
|
||||
|
||||
<button class="btn btn-sm btn-outline-danger"
|
||||
{% if list.is_archived %}disabled{% else %}onclick="deleteItem({{ item.id }})"{% endif %}>
|
||||
🗑️ Usuń
|
||||
</button>
|
||||
|
||||
</div>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
|
||||
<div class="input-group mb-2 position-relative">
|
||||
<input id="newItem" class="form-control" placeholder="Nowy produkt">
|
||||
<button onclick="addItem({{ list.id }})" class="btn btn-success">➕ Dodaj</button>
|
||||
</div>
|
||||
{% if not list.is_archived %}
|
||||
<div class="input-group mb-2 position-relative">
|
||||
<input id="newItem" class="form-control" placeholder="Nowy produkt">
|
||||
<button onclick="addItem({{ list.id }})" class="btn btn-success">➕ Dodaj</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% set receipt_pattern = 'list_' ~ list.id %}
|
||||
{% if receipt_files %}
|
||||
|
@ -2,31 +2,58 @@
|
||||
{% block title %}Lista: {{ list.title }}{% endblock %}
|
||||
{% block content %}
|
||||
|
||||
<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>
|
||||
</div>
|
||||
<h2 class="mb-2">
|
||||
🛍️ {{ list.title }}
|
||||
{% if list.is_archived %}
|
||||
<span class="badge bg-secondary ms-2">(Archiwalna)</span>
|
||||
{% endif %}
|
||||
{% if total_expense > 0 %}
|
||||
<span id="total-expense1" class="badge bg-success ms-2">
|
||||
💸 {{ '%.2f'|format(total_expense) }} PLN
|
||||
</span>
|
||||
{% else %}
|
||||
<span id="total-expense" class="badge bg-secondary ms-2" style="display: none;">
|
||||
💸 0.00 PLN
|
||||
</span>
|
||||
{% endif %}
|
||||
</h2>
|
||||
|
||||
<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 clickable-item {% if item.purchased %}bg-success text-white{% else %}bg-light text-white{% endif %}" id="item-{{ item.id }}">
|
||||
<div class="d-flex align-items-center gap-3 flex-grow-1">
|
||||
<input type="checkbox" class="form-check-input large-checkbox" {% if item.purchased %}checked{% endif %}>
|
||||
<input type="checkbox" class="form-check-input large-checkbox" {% if item.purchased %}checked{% endif %} {% if list.is_archived %}disabled{% endif %}>
|
||||
<span id="name-{{ item.id }}" class="{% if item.purchased %}text-white{% else %}text-white{% endif %}">{{ item.name }}</span>
|
||||
{% if item.note %}
|
||||
<small class="text-danger ms-4">[ Notatka: <b>{{ item.note }}</b> ]</small>
|
||||
{% endif %}
|
||||
</div>
|
||||
<button type="button" class="btn btn-sm btn-outline-info" onclick="openNoteModal(event, {{ item.id }})">📝 Notatka</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-info"
|
||||
{% if list.is_archived %}disabled{% else %}onclick="openNoteModal(event, {{ item.id }})"{% endif %}>
|
||||
📝 Notatka
|
||||
</button>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
<div class="input-group mb-2">
|
||||
<input id="newItem" class="form-control" placeholder="Nowy produkt">
|
||||
<button onclick="addItem({{ list.id }})" class="btn btn-success">➕ Dodaj</button>
|
||||
</div>
|
||||
{% if not list.is_archived %}
|
||||
<div class="input-group mb-2">
|
||||
<input id="newItem" class="form-control" placeholder="Nowy produkt">
|
||||
<button onclick="addItem({{ list.id }})" class="btn btn-success">➕ Dodaj</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
||||
{% if not list.is_archived %}
|
||||
<hr>
|
||||
<h5>💰 Dodaj wydatek</h5>
|
||||
<div class="input-group mb-2">
|
||||
<input id="expenseAmount" type="number" step="0.01" min="0" class="form-control" placeholder="Kwota (PLN)">
|
||||
<button onclick="submitExpense()" class="btn btn-success">💾 Zapisz</button>
|
||||
</div>
|
||||
<p id="total-expense2"><b>💸 Łącznie wydano:</b> {{ '%.2f'|format(total_expense) }} PLN</p>
|
||||
{% endif %}
|
||||
|
||||
<div id="toast-container" class="toast-container position-fixed bottom-0 end-0 p-3"></div>
|
||||
|
||||
{% set receipt_pattern = 'list_' ~ list.id %}
|
||||
{% if receipt_files %}
|
||||
@ -46,14 +73,16 @@
|
||||
<p><span class="badge bg-secondary">Brak wgranych paragonów do tej listy.</span></p>
|
||||
{% endif %}
|
||||
|
||||
<hr>
|
||||
<h5>📤 Dodaj zdjęcie paragonu</h5>
|
||||
<form action="{{ url_for('upload_receipt', list_id=list.id) }}" method="post" enctype="multipart/form-data">
|
||||
<div class="input-group mb-2">
|
||||
<input type="file" name="receipt" accept="image/*" capture="environment" class="form-control custom-file-input" id="receiptInput">
|
||||
<button type="submit" class="btn btn-success">➕ Wgraj</button>
|
||||
</div>
|
||||
</form>
|
||||
{% if not list.is_archived %}
|
||||
<hr>
|
||||
<h5>📤 Dodaj zdjęcie paragonu</h5>
|
||||
<form action="{{ url_for('upload_receipt', list_id=list.id) }}" method="post" enctype="multipart/form-data">
|
||||
<div class="input-group mb-2">
|
||||
<input type="file" name="receipt" accept="image/*" capture="environment" class="form-control custom-file-input" id="receiptInput">
|
||||
<button type="submit" class="btn btn-success">➕ Wgraj</button>
|
||||
</div>
|
||||
</form>
|
||||
{% endif %}
|
||||
|
||||
<!-- Modal notatki -->
|
||||
<div class="modal fade" id="noteModal" tabindex="-1" aria-hidden="true">
|
||||
@ -76,6 +105,8 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="toast-container" class="toast-container position-fixed bottom-0 end-0 p-3"></div>
|
||||
|
||||
<script>
|
||||
const LIST_ID = {{ list.id }};
|
||||
setupList(LIST_ID, 'Gość');
|
||||
|
Reference in New Issue
Block a user