zmiany ux oraz nowe funkcje

This commit is contained in:
Mateusz Gruszczyński
2025-08-18 14:08:33 +02:00
parent 8b1057d824
commit fc108bceb5
7 changed files with 149 additions and 90 deletions

24
app.py
View File

@@ -1748,6 +1748,18 @@ def create_list():
@app.route("/list/<int:list_id>")
@login_required
def view_list(list_id):
shopping_list = db.session.get(ShoppingList, list_id)
if not shopping_list:
abort(404)
is_owner = current_user.id == shopping_list.owner_id
if not is_owner:
flash("Nie jesteś właścicielem listy, przekierowano do widoku publicznego.", "warning")
if current_user.is_admin:
flash("W celu modyfikacji listy, przejdź do panelu administracyjnego.", "info")
return redirect(url_for("shared_list", token=shopping_list.share_token))
shopping_list, items, receipt_files, expenses, total_expense = get_list_details(
list_id
)
@@ -2541,6 +2553,10 @@ def add_user():
flash("Wypełnij wszystkie pola", "danger")
return redirect(url_for("list_users"))
if len(password) < 6:
flash("Hasło musi mieć co najmniej 6 znaków", "danger")
return redirect(url_for("list_users"))
if User.query.filter(func.lower(User.username) == username).first():
flash("Użytkownik o takiej nazwie już istnieje", "warning")
return redirect(url_for("list_users"))
@@ -2689,6 +2705,12 @@ def admin_receipts(id):
flash("Nieprawidłowe ID listy.", "danger")
return redirect(url_for("admin_panel"))
total_filesize = (
db.session.query(func.sum(Receipt.filesize)).scalar() or 0
)
page_filesize = sum(r.filesize or 0 for r in receipts_paginated)
query_string = urlencode({k: v for k, v in request.args.items() if k != "page"})
return render_template(
@@ -2701,6 +2723,8 @@ def admin_receipts(id):
total_pages=total_pages,
id=id,
query_string=query_string,
total_filesize=total_filesize,
page_filesize=page_filesize,
)

View File

@@ -62,11 +62,11 @@
</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>
<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>
@@ -114,7 +114,7 @@
<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> -->
<!-- <th title="Średnia kwota na 1 listę">Średnia</th> -->
</tr>
</thead>
<tbody>
@@ -123,21 +123,21 @@
<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> -->
<!-- <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> -->
<!-- <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> -->
<!-- <td>{{ '%.2f'|format(expense_summary.archived.avg) }} PLN</td> -->
</tr>
<tr>
<td>Wygasłe</td>
@@ -156,59 +156,59 @@
</div>
</div>
{# panel wyboru miesiąca zawsze widoczny #}
<div class="d-flex justify-content-between align-items-center mb-3 flex-wrap gap-2">
{# 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 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 %}
{% 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>
{# 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>
<div class="card bg-dark text-white mb-5">
<div class="card-body">
{# panel wyboru miesiąca zawsze widoczny #}
<div class="d-flex justify-content-between align-items-center mb-3 flex-wrap gap-2">
{# 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 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 %}
{% 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>
{# 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>
<h3 class="mt-4">
📄 Listy zakupowe
{% if show_all %}

View File

@@ -132,6 +132,7 @@
</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">

View File

@@ -80,18 +80,16 @@
</td>
</tr>
{% endif %}
</tbody>
</table>
</div>
</div>
</div>
<div class="mt-3">
<button type="submit" class="btn btn-success">💾 Zapisz</button>
<div>
<button type="submit" class="btn btn-sm btn-outline-light">💾 Zapisz zmiany</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>

View File

@@ -3,7 +3,35 @@
{% 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-light me-2">
Przelicz rozmiary plików
@@ -70,6 +98,7 @@
{% 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">
@@ -137,8 +166,10 @@
<img id="adminCropImage" style="max-width: 100%; max-height: 100%; display: block; margin: auto;">
</div>
<div class="modal-footer">
<button class="btn btn-secondary" data-bs-dismiss="modal">Anuluj</button>
<button class="btn btn-success" id="adminSaveCrop">Zapisz</button>
<div class="btn-group" role="group">
<button type="button" class="btn btn-sm btn-outline-light" data-bs-dismiss="modal">❌ Anuluj</button>
<button type="button" class="btn btn-sm btn-outline-light" id="adminSaveCrop">💾 Zapisz</button>
</div>
<div id="adminCropLoading" class="position-absolute top-50 start-50 translate-middle text-center d-none">
<div class="spinner-border text-light" role="status"></div>
<div class="mt-2 text-light">⏳ Pracuję...</div>

View File

@@ -10,25 +10,28 @@
<!-- Formularz dodawania nowego użytkownika -->
<div class="card bg-dark 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-light 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 sortable">
@@ -41,7 +44,6 @@
<th>Produkty</th>
<th>Paragony</th>
<th>Akcje</th>
</tr>
</thead>
<tbody>
@@ -71,15 +73,15 @@
<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>
<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>
<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 %}
</td>
</tr>
@@ -88,6 +90,7 @@
</table>
</div>
</div>
<!-- Modal resetowania hasła -->
<div class="modal fade" id="resetPasswordModal" tabindex="-1" aria-labelledby="resetPasswordModalLabel"
aria-hidden="true">
@@ -100,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>
@@ -111,7 +115,6 @@
</div>
{% block scripts %}
<script src="{{ url_for('static_bp.serve_js', filename='user_management.js') }}"></script>
{% endblock %}

View File

@@ -182,8 +182,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>