listy, i inne funkcje

This commit is contained in:
Mateusz Gruszczyński
2025-09-22 14:01:57 +02:00
parent 0b221696d4
commit 1a423a8b92
8 changed files with 877 additions and 64 deletions

View File

@@ -95,6 +95,10 @@
href="{{ url_for('dodaj_wydatek', zbiorka_id=z.id) }}">Dodaj
wydatek</a>
</li>
<li>
<a class="dropdown-item"
href="{{ url_for('transakcje_zbiorki', zbiorka_id=z.id) }}">Transakcje</a>
</li>
<li>
<a class="dropdown-item"
href="{{ url_for('edytuj_stan', zbiorka_id=z.id) }}">Edytuj stan</a>
@@ -147,7 +151,7 @@
{% endif %}
</div>
<!-- PANE: Zrealizowane -->
<!-- PANEL: Zrealizowane -->
<div class="tab-pane fade" id="pane-zrealizowane" role="tabpanel" aria-labelledby="tab-zrealizowane"
tabindex="0">
@@ -208,6 +212,10 @@
href="{{ url_for('dodaj_wydatek', zbiorka_id=z.id) }}">Dodaj
wydatek</a>
</li>
<li>
<a class="dropdown-item"
href="{{ url_for('transakcje_zbiorki', zbiorka_id=z.id) }}">Transakcje</a>
</li>
<li>
<a class="dropdown-item"
href="{{ url_for('edytuj_stan', zbiorka_id=z.id) }}">Edytuj stan</a>

View File

@@ -80,6 +80,98 @@
<hr class="my-4" />
<!-- SEKCJA: Lista produktów -->
<div class="mb-4">
<h6 class="text-muted mb-2">Lista produktów</h6>
<p class="text-muted small mb-3">
Wypunktuj dokładnie produkty do zakupu — podaj nazwę, opcjonalny link do sklepu i cenę.
Status domyślnie <em>Do kupienia</em>; przełącz na <em>Kupione</em> po realizacji.
</p>
<div class="table-responsive">
<table class="table align-middle" id="produkty-table">
<thead>
<tr>
<th style="min-width:220px;">Produkt</th>
<th style="min-width:240px;">Link do sklepu</th>
<th style="width:140px;">Cena [PLN]</th>
<th style="width:160px;">Status</th>
<th style="width:60px;"></th>
</tr>
</thead>
<tbody id="produkty-body">
{% set items = zbiorka.przedmioty if is_edit and zbiorka and zbiorka.przedmioty else []
%}
{% if items %}
{% for it in items %}
{% set i = loop.index0 %}
<tr>
<td>
<input type="text" class="form-control" name="item_nazwa[]"
value="{{ it.nazwa }}" placeholder="np. Karma Brit 10kg" required>
</td>
<td>
<input type="url" class="form-control" name="item_link[]"
value="{{ it.link or '' }}" placeholder="https://...">
</td>
<td>
<input type="text" inputmode="decimal" class="form-control text-end"
name="item_cena[]"
value="{{ (it.cena|round(2)) if it.cena is not none else '' }}"
placeholder="0,00">
</td>
<td>
<div class="form-check form-switch">
<input class="form-check-input kupione-switch" type="checkbox" {% if
it.kupione %}checked{% endif %}>
<input type="hidden" name="item_kupione_val_{{ i }}"
value="{{ 1 if it.kupione else 0 }}">
<label class="form-check-label small">{{ 'Kupione' if it.kupione else 'Do
kupienia' }}</label>
</div>
</td>
<td class="text-end">
<button type="button" class="btn btn-sm btn-outline-light border remove-row"
title="Usuń wiersz"></button>
</td>
</tr>
{% endfor %}
{% else %}
<!-- pusty wiersz startowy -->
<tr>
<td><input type="text" class="form-control" name="item_nazwa[]"
placeholder="np. Karma Brit 10kg" required></td>
<td><input type="url" class="form-control" name="item_link[]"
placeholder="https://..."></td>
<td><input type="text" inputmode="decimal" class="form-control text-end"
name="item_cena[]" placeholder="0,00"></td>
<td>
<div class="form-check form-switch">
<input class="form-check-input kupione-switch" type="checkbox">
<input type="hidden" name="item_kupione_val_0" value="0">
<label class="form-check-label small">Do kupienia</label>
</div>
</td>
<td class="text-end">
<button type="button" class="btn btn-sm btn-outline-light border remove-row"
title="Usuń wiersz"></button>
</td>
</tr>
{% endif %}
</tbody>
</table>
</div>
<div class="d-flex gap-2">
<button type="button" class="btn btn-sm btn-outline-light border" id="add-row">+ Dodaj
pozycję</button>
<button type="button" class="btn btn-sm btn-outline-light border" id="clear-empty">Usuń puste
wiersze</button>
</div>
</div>
<hr class="my-4" />
<!-- SEKCJA: Dane płatności -->
<div class="mb-4">
<h6 class="text-muted mb-2">Dane płatności</h6>
@@ -93,9 +185,8 @@
placeholder="12 3456 7890 1234 5678 9012 3456" required aria-describedby="ibanHelp"
value="{% if is_edit and zbiorka.numer_konta %}{{ zbiorka.numer_konta }}{% elif global_settings %}{{ global_settings.numer_konta }}{% else %}{% endif %}">
</div>
<div id="ibanHelp" class="form-text">
Wpisz ciąg cyfr; spacje dodadzą się automatycznie dla czytelności.
</div>
<div id="ibanHelp" class="form-text">Wpisz ciąg cyfr; spacje dodadzą się automatycznie dla
czytelności.</div>
</div>
<div class="col-12 col-md-6">
@@ -116,8 +207,9 @@
<button type="button" class="btn btn-sm btn-outline-light border" id="ustaw-globalne"
title="Wstaw wartości z ustawień globalnych" {% if global_settings %}
data-iban="{{ global_settings.numer_konta }}"
data-blik="{{ global_settings.numer_telefonu_blik }}" {% endif %}>Wstaw
globalne ustawienia</button>
data-blik="{{ global_settings.numer_telefonu_blik }}" {% endif %}>
Wstaw globalne ustawienia
</button>
</div>
{% endif %}
</div>
@@ -128,6 +220,16 @@
<!-- SEKCJA: Cel i widoczność -->
<div class="mb-4">
<h6 class="text-muted mb-2">Cel i widoczność</h6>
{# === BOX: zgodność sumy produktów z celem === #}
<div id="celSyncBox" class="alert d-none py-2 px-3 mb-3" role="alert">
<div class="d-flex flex-wrap align-items-center justify-content-between gap-2">
<div id="celSyncMsg" class="small"></div>
<button type="button" id="btnApplyCelFromSum"
class="btn btn-sm btn-outline-light border d-none"></button>
</div>
</div>
<div class="row g-3">
<div class="col-12 col-md-6">
<label for="cel" class="form-label">Cel zbiórki</label>
@@ -153,10 +255,9 @@
<!-- CTA -->
<div class="d-flex flex-wrap gap-2">
<button type="submit" class="btn btn-success">
{{ ' Zaktualizuj zbiórkę' if is_edit else 'Dodaj zbiórkę' }} </button>
{{ ' Zaktualizuj zbiórkę' if is_edit else 'Dodaj zbiórkę' }}
</button>
<a href="{{ url_for('admin_dashboard') }}" class="btn btn-outline-light border">Anuluj</a>
</div>
</form>
</div>
@@ -169,4 +270,6 @@
<script src="https://cdn.jsdelivr.net/simplemde/latest/simplemde.min.js"></script>
<script src="{{ url_for('static', filename='js/mde_custom.js') }}"></script>
<script src="{{ url_for('static', filename='js/formularz_zbiorek.js') }}"></script>
<script src="{{ url_for('static', filename='js/produkty_formularz.js') }}"></script>
<script src="{{ url_for('static', filename='js/kwoty_formularz.js') }}"></script>
{% endblock %}

View File

@@ -0,0 +1,143 @@
{% extends 'base.html' %}
{% block title %}Transakcje {{ zbiorka.nazwa }}{% endblock %}
{% block content %}
<div class="container my-4">
<div class="d-flex justify-content-between align-items-center mb-3">
<h3 class="mb-0">Transakcje: {{ zbiorka.nazwa }}</h3>
<div class="btn-group">
<a class="btn btn-sm btn-outline-light border" href="{{ url_for('dodaj_wplate', zbiorka_id=zbiorka.id) }}">+
Wpłata</a>
<a class="btn btn-sm btn-outline-light border"
href="{{ url_for('dodaj_wydatek', zbiorka_id=zbiorka.id) }}">+ Wydatek</a>
<a class="btn btn-sm btn-outline-light border"
href="{{ url_for('zbiorka', zbiorka_id=zbiorka.id) }}">Szczegóły zbiórki</a>
</div>
</div>
<div class="card shadow-sm">
<div class="card-body">
<div class="table-responsive">
<table class="table align-middle">
<thead>
<tr>
<th>Data</th>
<th>Typ</th>
<th class="text-end">Kwota</th>
<th>Opis</th>
<th class="text-end"></th>
</tr>
</thead>
<tbody>
{% for a in aktywnosci %}
<tr>
<td>{{ a.data|dt("%d.%m.%Y %H:%M") }}</td>
<td>
<span class="badge {{ 'bg-success' if a.typ=='wpłata' else 'bg-danger' }}">{{ a.typ
}}</span>
</td>
<td class="text-end">{{ '%.2f'|format(a.kwota) }} PLN</td>
<td class="text-muted">{{ a.opis or '—' }}</td>
<td class="text-end">
{% if a.typ == 'wpłata' %}
<button class="btn btn-sm btn-outline-light border btn-edit-wplata" data-id="{{ a.id }}"
data-kwota="{{ '%.2f'|format(a.kwota) }}" data-opis="{{ a.opis|e if a.opis }}"
data-action="{{ url_for('zapisz_wplate', wplata_id=a.id) }}">
Edytuj
</button>
<form class="d-inline" method="post"
action="{{ url_for('usun_wplate', wplata_id=a.id) }}"
onsubmit="return confirm('Usunąć wpłatę? Cofnie to wpływ na stan.');">
<button class="btn btn-sm btn-outline-danger">Usuń</button>
</form>
{% else %}
<button class="btn btn-sm btn-outline-light border btn-edit-wydatek"
data-id="{{ a.id }}" data-kwota="{{ '%.2f'|format(a.kwota) }}"
data-opis="{{ a.opis|e if a.opis }}"
data-action="{{ url_for('zapisz_wydatek', wydatek_id=a.id) }}">
Edytuj
</button>
<form class="d-inline" method="post"
action="{{ url_for('usun_wydatek', wydatek_id=a.id) }}"
onsubmit="return confirm('Usunąć wydatek? Cofnie to wpływ na stan.');">
<button class="btn btn-sm btn-outline-danger">Usuń</button>
</form>
{% endif %}
</td>
</tr>
{% else %}
<tr>
<td colspan="5" class="text-center text-muted py-4">Brak transakcji.</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="small text-muted">
Aktualny stan: <strong>{{ '%.2f'|format(zbiorka.stan or 0) }} PLN</strong>
</div>
</div>
</div>
</div>
{# === MODAL: Edycja wpłaty === #}
<div class="modal fade" id="modalWplata" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<form class="modal-content" method="post" id="formWplata">
<div class="modal-header">
<h5 class="modal-title">Edytuj wpłatę</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Zamknij"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label class="form-label">Kwota (PLN)</label>
<input class="form-control text-end" name="kwota" step="0.01" min="0.01" id="wplataKwota"
inputmode="decimal" required>
</div>
<div class="mb-3">
<label class="form-label">Opis</label>
<textarea class="form-control" name="opis" id="wplataOpis" rows="3"></textarea>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-success">Zapisz</button>
<button type="button" class="btn btn-outline-light border" data-bs-dismiss="modal">Anuluj</button>
</div>
</form>
</div>
</div>
{# === MODAL: Edycja wydatku === #}
<div class="modal fade" id="modalWydatek" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<form class="modal-content" method="post" id="formWydatek">
<div class="modal-header">
<h5 class="modal-title">Edytuj wydatek</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Zamknij"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label class="form-label">Kwota (PLN)</label>
<input class="form-control text-end" name="kwota" step="0.01" min="0.01" id="wydatekKwota"
inputmode="decimal" required>
</div>
<div class="mb-3">
<label class="form-label">Opis</label>
<textarea class="form-control" name="opis" id="wydatekOpis" rows="3"></textarea>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-success">Zapisz</button>
<button type="button" class="btn btn-outline-light border" data-bs-dismiss="modal">Anuluj</button>
</div>
</form>
</div>
</div>
{% endblock %}
{% block extra_scripts %}
{{ super() }}
<script src="{{ url_for('static', filename='js/transakcje.js') }}"></script>
{% endblock %}

View File

@@ -4,7 +4,7 @@
{% block content %}
<div class="container my-4">
{# Postęp 0100 #}
{# Wyliczenia postępu finansowego #}
{% set has_cel = (zbiorka.cel is defined and zbiorka.cel and zbiorka.cel > 0) %}
{% set progress = (zbiorka.stan / zbiorka.cel * 100) if has_cel else 0 %}
{% set progress_clamped = 100 if progress > 100 else (0 if progress < 0 else progress) %} {% set
@@ -26,32 +26,138 @@
</div>
<div class="row g-4">
<!-- Kolumna: opis + progress -->
<!-- Kolumna lewa: Opis + (opcjonalnie) Lista zakupów + Postęp -->
<div class="col-md-8">
<div class="card shadow-sm h-100">
<!-- Card: Opis -->
<div class="card shadow-sm mb-4">
<div class="card-body">
<h5 class="mb-2">Opis</h5>
<div class="mb-4">
<div class="mb-0">
{{ zbiorka.opis | markdown }}
</div>
<h5 class="mb-2">Postęp</h5>
<div class="progress mb-2" role="progressbar" aria-valuenow="{{ progress_clamped|round(2) }}"
aria-valuemin="0" aria-valuemax="100" aria-label="Postęp zbiórki {{ progress_clamped|round(0) }} procent">
<div class="progress-bar" style="width: {{ progress_clamped }}%;"></div>
</div>
<small class="text-muted">
{% if zbiorka.ukryj_kwote %}
{% else %}
{{ progress|round(1) }}%
{% endif %}
</small>
</div>
</div>
{# Czy są produkty? #}
{% set items = zbiorka.przedmioty or [] %}
{% set has_items = (items|length > 0) %}
<!-- Card: Lista zakupów (tylko gdy są produkty) -->
{% if has_items %}
<div class="card shadow-sm mb-4">
<div class="card-body">
<h5 class="mb-2">Lista zakupów</h5>
{% set posortowane = items|sort(attribute='kupione') %}
<ul class="list-group list-group-flush">
{% for it in posortowane %}
<li class="list-group-item bg-transparent d-flex flex-wrap justify-content-between align-items-center">
<div class="d-flex align-items-center gap-2">
{% if it.kupione %}
<span class="badge bg-success">Kupione</span>
{% else %}
<span class="badge bg-warning text-dark">Do kupienia</span>
{% endif %}
<span class="fw-semibold">{{ it.nazwa }}</span>
{% if it.link %}
<a href="{{ it.link }}" target="_blank" rel="noopener"
class="btn btn-sm btn-outline-light border ms-2">Sklep ↗</a>
{% endif %}
</div>
<div>
{% if not zbiorka.ukryj_kwote %}
{% if it.cena is not none %}
<span class="badge bg-dark border" style="border-color: var(--border);">
{{ it.cena|round(2) }} PLN
</span>
{% else %}
<span class="text-muted"></span>
{% endif %}
{% else %}
<span class="text-muted"></span>
{% endif %}
</div>
</li>
{% endfor %}
</ul>
</div>
</div>
{% endif %}
<!-- Card: Postęp (POD listą zakupów) -->
{# Dodatkowe wyliczenia do postępu zakupów #}
{% set total_cnt = items|length %}
{% set kupione_cnt = (items|selectattr('kupione')|list|length) %}
{% set items_pct = (kupione_cnt / total_cnt * 100) if total_cnt > 0 else 0 %}
{% if not zbiorka.ukryj_kwote %}
{% set suma_all = (items|selectattr('cena')|map(attribute='cena')|sum) or 0 %}
{% set suma_kupione = (items|selectattr('kupione')|selectattr('cena')|map(attribute='cena')|sum) or 0 %}
{% set suma_pct = (suma_kupione / suma_all * 100) if suma_all > 0 else 0 %}
{% endif %}
<div class="card shadow-sm">
<div class="card-body">
<div class="d-flex flex-wrap align-items-center justify-content-between gap-2 mb-2">
<h5 class="mb-0">Postęp</h5>
<div class="d-flex flex-wrap align-items-center gap-2">
{% if has_cel and not zbiorka.ukryj_kwote %}
<span class="badge bg-dark border" style="border-color: var(--border);">
Finanse: {{ zbiorka.stan|round(2) }} / {{ zbiorka.cel|round(2) }} PLN
</span>
{% endif %}
{% if has_items %}
<span class="badge bg-secondary">Pozycje: {{ kupione_cnt }}/{{ total_cnt }}</span>
{% if not zbiorka.ukryj_kwote and (suma_all or 0) > 0 %}
<span class="badge bg-secondary">Zakupy (kwotowo):
{{ (suma_kupione or 0)|round(2) }} / {{ (suma_all or 0)|round(2) }} PLN
</span>
{% endif %}
{% endif %}
</div>
</div>
<!-- Pasek: Finanse (zawsze) -->
<div class="mb-3">
<small class="text-muted">Finanse</small>
<div class="progress" role="progressbar" aria-valuenow="{{ progress_clamped|round(2) }}" aria-valuemin="0"
aria-valuemax="100">
<div class="progress-bar" style="width: {{ progress_clamped }}%;"></div>
</div>
<small class="text-muted">
{% if zbiorka.ukryj_kwote %}—{% else %}{{ progress|round(1) }}%{% endif %}
</small>
</div>
{% if has_items %}
<!-- Pasek: Zakupy sztukami -->
<div class="mb-3">
<small class="text-muted">Zakupy (liczba pozycji)</small>
<div class="progress" role="progressbar" aria-valuenow="{{ items_pct|round(2) }}" aria-valuemin="0"
aria-valuemax="100">
<div class="progress-bar" style="width: {{ items_pct }}%;"></div>
</div>
<small class="text-muted">{{ items_pct|round(1) }}%</small>
</div>
{% if not zbiorka.ukryj_kwote and (suma_all or 0) > 0 %}
<!-- Pasek: Zakupy kwotowo -->
<div>
<small class="text-muted">Zakupy (kwotowo)</small>
<div class="progress" role="progressbar" aria-valuenow="{{ suma_pct|round(2) }}" aria-valuemin="0"
aria-valuemax="100">
<div class="progress-bar" style="width: {{ suma_pct }}%;"></div>
</div>
<small class="text-muted">{{ suma_pct|round(1) }}%</small>
</div>
{% endif %}
{% endif %}
</div>
</div>
</div>
<!-- Kolumna: płatności (sticky) -->
<!-- Kolumna prawa: płatności (sticky) -->
<div class="col-md-4">
<div class="card shadow-sm wspomoz-card position-sticky" style="top: 1rem;">
<div class="card-body">
@@ -63,7 +169,6 @@
</div>
<div class="fs-5" id="ibanDisplay">{{ zbiorka.numer_konta }}</div>
</div>
<div class="mb-3">
<div class="d-flex align-items-center justify-content-between">
<strong>Telefon BLIK</strong>
@@ -109,10 +214,10 @@
</div>
</div>
<!-- Aktywność (wpłaty + wydatki) -->
<!-- Aktywność -->
<div class="card shadow-sm mt-4">
<div class="card-header d-flex align-items-center justify-content-between">
<h5 class="card-title mb-0">Aktywność</h5>
<h5 class="card-title mb-0">Aktywność / Transakcje</h5>
{% if aktywnosci and aktywnosci|length > 0 %}
<small class="text-muted">Łącznie pozycji: {{ aktywnosci|length }}</small>
{% endif %}
@@ -123,7 +228,7 @@
{% for a in aktywnosci %}
<li class="list-group-item bg-transparent d-flex flex-wrap justify-content-between align-items-center">
<div class="me-3">
<strong>{{ a.data.strftime('%Y-%m-%d %H:%M:%S') }}</strong>
<strong>{{ a.data|dt("%d.%m.%Y %H:%M") }}</strong>
<span class="badge {% if a.typ == 'wpłata' %}bg-success{% else %}bg-danger{% endif %} ms-2">
{{ a.typ|capitalize }}
</span>