progress bar and stats
This commit is contained in:
@@ -821,7 +821,7 @@ def get_progress(list_id: int) -> tuple[int, int, float]:
|
||||
result = (
|
||||
db.session.query(
|
||||
func.count(Item.id),
|
||||
func.sum(case((Item.purchased == True, 1), else_=0)),
|
||||
func.sum(case(((Item.purchased == True) & (Item.not_purchased == False), 1), else_=0)),
|
||||
)
|
||||
.filter(Item.list_id == list_id)
|
||||
.first()
|
||||
|
||||
@@ -682,6 +682,12 @@ def edit_list(list_id):
|
||||
elif action == "toggle_purchased":
|
||||
item = get_valid_item_or_404(request.form.get("item_id"), list_id)
|
||||
item.purchased = not item.purchased
|
||||
if item.purchased:
|
||||
item.not_purchased = False
|
||||
item.not_purchased_reason = None
|
||||
item.purchased_at = utcnow()
|
||||
else:
|
||||
item.purchased_at = None
|
||||
db.session.commit()
|
||||
flash("Zmieniono status oznaczenia produktu", "success")
|
||||
return redirect(url_for("edit_list", list_id=list_id))
|
||||
|
||||
@@ -115,7 +115,7 @@ def main_page():
|
||||
db.session.query(
|
||||
Item.list_id,
|
||||
func.count(Item.id).label("total_count"),
|
||||
func.sum(case((Item.purchased == True, 1), else_=0)).label(
|
||||
func.sum(case((((Item.purchased == True) & (Item.not_purchased == False)), 1), else_=0)).label(
|
||||
"purchased_count"
|
||||
),
|
||||
func.sum(case((Item.not_purchased == True, 1), else_=0)).label(
|
||||
@@ -163,6 +163,28 @@ def main_page():
|
||||
l.total_expense = 0
|
||||
l.category_badges = []
|
||||
|
||||
def build_progress_summary(lists_):
|
||||
total_lists = len(lists_)
|
||||
total_products = sum(getattr(l, "total_count", 0) or 0 for l in lists_)
|
||||
purchased_products = sum(getattr(l, "purchased_count", 0) or 0 for l in lists_)
|
||||
not_purchased_products = sum(getattr(l, "not_purchased_count", 0) or 0 for l in lists_)
|
||||
total_expense = float(sum((getattr(l, "total_expense", 0) or 0) for l in lists_))
|
||||
completion_percent = (
|
||||
(purchased_products / total_products) * 100 if total_products > 0 else 0
|
||||
)
|
||||
return {
|
||||
"list_count": total_lists,
|
||||
"total_products": total_products,
|
||||
"purchased_products": purchased_products,
|
||||
"not_purchased_products": not_purchased_products,
|
||||
"remaining_products": max(total_products - purchased_products - not_purchased_products, 0),
|
||||
"total_expense": round(total_expense, 2),
|
||||
"completion_percent": completion_percent,
|
||||
}
|
||||
|
||||
user_lists_summary = build_progress_summary(user_lists)
|
||||
accessible_lists_summary = build_progress_summary(accessible_lists)
|
||||
|
||||
expiring_lists = get_expiring_lists_for_user(current_user.id) if current_user.is_authenticated else []
|
||||
templates = (ListTemplate.query.filter_by(is_active=True, created_by=current_user.id).order_by(ListTemplate.name.asc()).all() if current_user.is_authenticated else [])
|
||||
|
||||
@@ -178,6 +200,8 @@ def main_page():
|
||||
selected_month=month_str,
|
||||
expiring_lists=expiring_lists,
|
||||
templates=templates,
|
||||
user_lists_summary=user_lists_summary,
|
||||
accessible_lists_summary=accessible_lists_summary,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -345,6 +345,8 @@ def handle_check_item(data):
|
||||
if item:
|
||||
item.purchased = True
|
||||
item.purchased_at = datetime.now(UTC)
|
||||
item.not_purchased = False
|
||||
item.not_purchased_reason = None
|
||||
log_list_activity(item.list_id, 'item_checked', item_name=item.name, actor=current_user if current_user.is_authenticated else None, actor_name=current_user.username if current_user.is_authenticated else 'Gość')
|
||||
db.session.commit()
|
||||
|
||||
@@ -470,6 +472,8 @@ def handle_mark_not_purchased(data):
|
||||
if item:
|
||||
item.not_purchased = True
|
||||
item.not_purchased_reason = reason
|
||||
item.purchased = False
|
||||
item.purchased_at = None
|
||||
log_list_activity(item.list_id, 'item_marked_not_purchased', item_name=item.name, actor=current_user if current_user.is_authenticated else None, actor_name=current_user.username if current_user.is_authenticated else 'Gość', details=reason or None)
|
||||
db.session.commit()
|
||||
emit(
|
||||
|
||||
@@ -5701,3 +5701,125 @@ body:not(.sorting-active) .drag-handle {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* --- Main page list progress consistency --- */
|
||||
.endpoint-main_page .list-group-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.endpoint-main_page .main-list-progress-wrap {
|
||||
display: block;
|
||||
width: 100%;
|
||||
flex: 0 0 100%;
|
||||
margin-top: 0.8rem !important;
|
||||
}
|
||||
|
||||
.endpoint-main_page .list-group-item > .main-list-row + .main-list-progress-wrap {
|
||||
align-self: stretch;
|
||||
}
|
||||
|
||||
.endpoint-main_page .main-list-progress {
|
||||
width: 100%;
|
||||
height: 16px;
|
||||
margin-top: 0 !important;
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
background:
|
||||
linear-gradient(180deg, rgba(255, 255, 255, 0.05), rgba(255, 255, 255, 0.02)),
|
||||
var(--dark-700) !important;
|
||||
box-shadow:
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.05),
|
||||
0 4px 10px rgba(0, 0, 0, 0.18);
|
||||
}
|
||||
|
||||
.endpoint-main_page .main-list-progress .progress-bar.bg-success {
|
||||
background: linear-gradient(135deg, rgba(40, 199, 111, 0.98), rgba(22, 163, 74, 0.98)) !important;
|
||||
}
|
||||
|
||||
.endpoint-main_page .main-list-progress .progress-bar.bg-warning {
|
||||
background: linear-gradient(135deg, rgba(245, 189, 65, 0.98), rgba(217, 119, 6, 0.98)) !important;
|
||||
}
|
||||
|
||||
.endpoint-main_page .main-list-progress .progress-bar.bg-transparent {
|
||||
background: rgba(255, 255, 255, 0.08) !important;
|
||||
}
|
||||
|
||||
.endpoint-main_page .main-list-progress__label {
|
||||
max-width: calc(100% - 0.85rem);
|
||||
padding: 0 0.45rem;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
text-align: center;
|
||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.45);
|
||||
letter-spacing: 0.01em;
|
||||
}
|
||||
|
||||
@media (max-width: 575.98px) {
|
||||
.endpoint-main_page .main-list-progress {
|
||||
height: 15px;
|
||||
}
|
||||
|
||||
.endpoint-main_page .main-list-progress__label {
|
||||
font-size: 0.64rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* --- Main page progress summary cards --- */
|
||||
.endpoint-main_page #mainStatsCollapse.collapsing,
|
||||
.endpoint-main_page #mainStatsCollapse.show {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.endpoint-main_page .main-summary-card {
|
||||
height: 100%;
|
||||
padding: 1rem 1rem 1.05rem;
|
||||
border-radius: 1rem;
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
background: linear-gradient(180deg, rgba(255,255,255,0.04), rgba(255,255,255,0.02)), rgba(9, 16, 28, 0.88);
|
||||
box-shadow: 0 12px 28px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.endpoint-main_page .main-summary-card__eyebrow {
|
||||
font-size: 0.72rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
color: rgba(255, 255, 255, 0.65);
|
||||
margin-bottom: 0.2rem;
|
||||
}
|
||||
|
||||
.endpoint-main_page .main-summary-card__title {
|
||||
font-size: 1.05rem;
|
||||
}
|
||||
|
||||
.endpoint-main_page .main-summary-stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 0.7rem;
|
||||
}
|
||||
|
||||
.endpoint-main_page .main-summary-stat {
|
||||
padding: 0.65rem 0.75rem;
|
||||
border-radius: 0.85rem;
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
border: 1px solid rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
|
||||
.endpoint-main_page .main-summary-stat__label {
|
||||
display: block;
|
||||
font-size: 0.73rem;
|
||||
color: rgba(255, 255, 255, 0.66);
|
||||
margin-bottom: 0.15rem;
|
||||
}
|
||||
|
||||
|
||||
@media (max-width: 575.98px) {
|
||||
.endpoint-main_page .main-summary-card {
|
||||
padding: 0.9rem;
|
||||
}
|
||||
|
||||
.endpoint-main_page .main-summary-stats {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
31
shopping_app/templates/_list_progress.html
Normal file
31
shopping_app/templates/_list_progress.html
Normal file
@@ -0,0 +1,31 @@
|
||||
{% set total_count = total_count or 0 %}
|
||||
{% set purchased_count = purchased_count or 0 %}
|
||||
{% set not_purchased_count = not_purchased_count or 0 %}
|
||||
{% set accounted_count = purchased_count + not_purchased_count %}
|
||||
{% set percent = (purchased_count / total_count * 100) if total_count > 0 else 0 %}
|
||||
{% set purchased_percent = (purchased_count / total_count * 100) if total_count > 0 else 0 %}
|
||||
{% set not_purchased_percent = (not_purchased_count / total_count * 100) if total_count > 0 else 0 %}
|
||||
{% set remaining_count = (total_count - accounted_count) if total_count > accounted_count else 0 %}
|
||||
{% set remaining_percent = (remaining_count / total_count * 100) if total_count > 0 else 100 %}
|
||||
|
||||
<div class="main-list-progress-wrap mt-2">
|
||||
<div class="main-list-progress progress progress-dark progress-thin position-relative"
|
||||
aria-label="Postęp listy {{ purchased_count }} z {{ total_count }} kupionych">
|
||||
<div class="progress-bar bg-success" role="progressbar"
|
||||
style="width: {{ purchased_percent }}%"
|
||||
aria-valuemin="0" aria-valuemax="100"></div>
|
||||
|
||||
<div class="progress-bar bg-warning" role="progressbar"
|
||||
style="width: {{ not_purchased_percent }}%"
|
||||
aria-valuemin="0" aria-valuemax="100"></div>
|
||||
|
||||
<div class="progress-bar bg-transparent" role="progressbar"
|
||||
style="width: {{ remaining_percent }}%"
|
||||
aria-valuemin="0" aria-valuemax="100"></div>
|
||||
|
||||
<span class="progress-label main-list-progress__label small fw-bold {% if percent < 51 %}text-white{% else %}text-dark{% endif %}">
|
||||
Produkty: {{ purchased_count }}/{{ total_count }} ({{ percent|round(0) }}%)
|
||||
{% if total_expense > 0 %} — 💸 {{ '%.2f'|format(total_expense) }} PLN{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -69,12 +69,61 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{% macro render_summary_panel(title, summary, accent='success') -%}
|
||||
<div class="col-12 col-lg-6">
|
||||
<div class="main-summary-card h-100">
|
||||
<div class="d-flex justify-content-between align-items-start gap-3 flex-wrap mb-2">
|
||||
<div>
|
||||
<div class="main-summary-card__eyebrow">Postęp</div>
|
||||
<h4 class="main-summary-card__title mb-0">{{ title }}</h4>
|
||||
</div>
|
||||
<span class="badge rounded-pill text-bg-dark border border-secondary-subtle">Listy: {{ summary.list_count }}</span>
|
||||
</div>
|
||||
|
||||
{% with total_count=summary.total_products, purchased_count=summary.purchased_products, not_purchased_count=summary.not_purchased_products, total_expense=summary.total_expense %}
|
||||
{% include '_list_progress.html' %}
|
||||
{% endwith %}
|
||||
|
||||
<div class="main-summary-stats mt-3">
|
||||
<div class="main-summary-stat">
|
||||
<span class="main-summary-stat__label">Kupione</span>
|
||||
<strong>{{ summary.purchased_products }}</strong>
|
||||
</div>
|
||||
<div class="main-summary-stat">
|
||||
<span class="main-summary-stat__label">Niekupione</span>
|
||||
<strong>{{ summary.not_purchased_products }}</strong>
|
||||
</div>
|
||||
<div class="main-summary-stat">
|
||||
<span class="main-summary-stat__label">Nieoznaczone</span>
|
||||
<strong>{{ summary.remaining_products }}</strong>
|
||||
</div>
|
||||
<div class="main-summary-stat">
|
||||
<span class="main-summary-stat__label">Wydatki</span>
|
||||
<strong>{{ '%.2f'|format(summary.total_expense) }} PLN</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{%- endmacro %}
|
||||
|
||||
{% if current_user.is_authenticated %}
|
||||
<h3 class="mt-4 d-flex justify-content-between align-items-center flex-wrap">
|
||||
Twoje listy
|
||||
<button type="button" class="btn btn-sm btn-outline-light ms-2" data-bs-toggle="modal" data-bs-target="#archivedModal">
|
||||
<div class="d-flex justify-content-end align-items-center flex-wrap gap-2 mb-3">
|
||||
<button class="btn btn-sm btn-outline-light" type="button" data-bs-toggle="collapse" data-bs-target="#mainStatsCollapse" aria-expanded="false" aria-controls="mainStatsCollapse">
|
||||
📈 Statystyki
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-light" data-bs-toggle="modal" data-bs-target="#archivedModal">
|
||||
🗄️ Zarchiwizowane
|
||||
</button>
|
||||
</div>
|
||||
<div class="collapse mb-4" id="mainStatsCollapse">
|
||||
<div class="row g-3">
|
||||
{{ render_summary_panel('Twoje listy', user_lists_summary) }}
|
||||
{{ render_summary_panel('Udostępnione i publiczne', accessible_lists_summary, 'info') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 class="mt-4 d-flex justify-content-between align-items-center flex-wrap">
|
||||
Twoje listy
|
||||
</h3>
|
||||
|
||||
{% if user_lists %}
|
||||
@@ -127,31 +176,28 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="progress progress-dark progress-thin mt-2 position-relative">
|
||||
<div class="progress-bar bg-success" role="progressbar"
|
||||
style="width: {{ (purchased_count / total_count * 100) if total_count > 0 else 0 }}%"
|
||||
aria-valuemin="0" aria-valuemax="100"></div>
|
||||
|
||||
{% set not_purchased_count = l.not_purchased_count if l.total_count else 0 %}
|
||||
<div class="progress-bar bg-warning" role="progressbar"
|
||||
style="width: {{ (not_purchased_count / total_count * 100) if total_count > 0 else 0 }}%"
|
||||
aria-valuemin="0" aria-valuemax="100"></div>
|
||||
|
||||
<div class="progress-bar bg-transparent" role="progressbar"
|
||||
style="width: {{ 100 - ((purchased_count + not_purchased_count) / total_count * 100) if total_count > 0 else 100 }}%"
|
||||
aria-valuemin="0" aria-valuemax="100"></div>
|
||||
|
||||
<span class="progress-label small fw-bold {% if percent < 51 %}text-white{% else %}text-dark{% endif %}">
|
||||
Produkty: {{ purchased_count }}/{{ total_count }} ({{ percent|round(0) }}%)
|
||||
{% if l.total_expense > 0 %} — 💸 {{ '%.2f'|format(l.total_expense) }} PLN{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
{% with total_count=total_count, purchased_count=purchased_count, not_purchased_count=l.not_purchased_count, total_expense=l.total_expense %}
|
||||
{% include '_list_progress.html' %}
|
||||
{% endwith %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<p><span class="badge rounded-pill bg-secondary opacity-75">Nie utworzono żadnej listy</span></p>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<div class="mb-3">
|
||||
<button class="btn btn-sm btn-outline-light" type="button" data-bs-toggle="collapse" data-bs-target="#mainStatsCollapse" aria-expanded="false" aria-controls="mainStatsCollapse">
|
||||
📈 Statystyki
|
||||
</button>
|
||||
</div>
|
||||
<div class="collapse mb-4" id="mainStatsCollapse">
|
||||
<div class="row g-3">
|
||||
<div class="col-12">
|
||||
{{ render_summary_panel('Publiczne listy innych użytkowników', accessible_lists_summary, 'info') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<h3 class="mt-4">
|
||||
@@ -201,25 +247,9 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="progress progress-dark progress-thin mt-2 position-relative">
|
||||
<div class="progress-bar bg-success" role="progressbar"
|
||||
style="width: {{ (purchased_count / total_count * 100) if total_count > 0 else 0 }}%"
|
||||
aria-valuemin="0" aria-valuemax="100"></div>
|
||||
|
||||
{% set not_purchased_count = l.not_purchased_count if l.total_count else 0 %}
|
||||
<div class="progress-bar bg-warning" role="progressbar"
|
||||
style="width: {{ (not_purchased_count / total_count * 100) if total_count > 0 else 0 }}%"
|
||||
aria-valuemin="0" aria-valuemax="100"></div>
|
||||
|
||||
<div class="progress-bar bg-transparent" role="progressbar"
|
||||
style="width: {{ 100 - ((purchased_count + not_purchased_count) / total_count * 100) if total_count > 0 else 100 }}%"
|
||||
aria-valuemin="0" aria-valuemax="100"></div>
|
||||
|
||||
<span class="progress-label small fw-bold {% if percent < 51 %}text-white{% else %}text-dark{% endif %}">
|
||||
Produkty: {{ purchased_count }}/{{ total_count }} ({{ percent|round(0) }}%)
|
||||
{% if l.total_expense > 0 %} — 💸 {{ '%.2f'|format(l.total_expense) }} PLN{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
{% with total_count=total_count, purchased_count=purchased_count, not_purchased_count=l.not_purchased_count, total_expense=l.total_expense %}
|
||||
{% include '_list_progress.html' %}
|
||||
{% endwith %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
Reference in New Issue
Block a user