Compare commits
5 Commits
f1744fae99
...
master
Author | SHA1 | Date | |
---|---|---|---|
![]() |
68f235d605 | ||
![]() |
ea46dd43e1 | ||
![]() |
4b99b109bd | ||
![]() |
028ae3c26e | ||
![]() |
71b14411e5 |
32
app.py
32
app.py
@@ -960,6 +960,12 @@ def get_active_months_query(visible_lists_query=None):
|
||||
return [row.month for row in active_months]
|
||||
|
||||
|
||||
def normalize_name(name):
|
||||
if not name:
|
||||
return ""
|
||||
return re.sub(r'\s+', ' ', name).strip().lower()
|
||||
|
||||
|
||||
############# OCR ###########################
|
||||
|
||||
|
||||
@@ -2849,23 +2855,20 @@ def list_products():
|
||||
seen_names = set()
|
||||
unique_items = []
|
||||
for item in all_items:
|
||||
key = (item.name or "").strip().lower()
|
||||
key = normalize_name(item.name)
|
||||
if key not in seen_names:
|
||||
unique_items.append(item)
|
||||
seen_names.add(key)
|
||||
|
||||
stmt = (
|
||||
select(
|
||||
func.lower(func.trim(Item.name)).label("name_lower"),
|
||||
func.coalesce(func.sum(Item.quantity), 0).label("qty_sum"),
|
||||
usage_counts = dict(
|
||||
db.session.query(
|
||||
func.lower(Item.name),
|
||||
func.coalesce(func.sum(Item.quantity), 0)
|
||||
)
|
||||
.where(Item.name.isnot(None))
|
||||
.group_by(func.lower(func.trim(Item.name)))
|
||||
.group_by(func.lower(Item.name))
|
||||
.all()
|
||||
)
|
||||
|
||||
rows = db.session.execute(stmt).all()
|
||||
usage_counts = {name_lower: qty_sum for name_lower, qty_sum in rows}
|
||||
|
||||
total_items = len(unique_items)
|
||||
total_pages = (total_items + per_page - 1) // per_page
|
||||
start = (page - 1) * per_page
|
||||
@@ -2877,12 +2880,13 @@ def list_products():
|
||||
users_dict = {u.id: u.username for u in users}
|
||||
|
||||
suggestions = SuggestedProduct.query.all()
|
||||
|
||||
all_suggestions_dict = {
|
||||
(s.name or "").strip().lower(): s
|
||||
normalize_name(s.name): s
|
||||
for s in suggestions
|
||||
if s.name and s.name.strip()
|
||||
}
|
||||
used_suggestion_names = {(i.name or "").strip().lower() for i in unique_items}
|
||||
used_suggestion_names = {normalize_name(i.name) for i in unique_items}
|
||||
|
||||
suggestions_dict = {
|
||||
name: all_suggestions_dict[name]
|
||||
@@ -2896,6 +2900,7 @@ def list_products():
|
||||
|
||||
|
||||
query_string = urlencode({k: v for k, v in request.args.items() if k != "page"})
|
||||
synced_names = set(suggestions_dict.keys())
|
||||
|
||||
return render_template(
|
||||
"admin/list_products.html",
|
||||
@@ -2908,7 +2913,8 @@ def list_products():
|
||||
total_pages=total_pages,
|
||||
query_string=query_string,
|
||||
total_items=total_items,
|
||||
usage_counts=usage_counts
|
||||
usage_counts=usage_counts,
|
||||
synced_names=synced_names
|
||||
)
|
||||
|
||||
|
||||
|
@@ -1,73 +1,91 @@
|
||||
function bindSyncButton(button) {
|
||||
button.addEventListener('click', function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
const itemId = button.getAttribute('data-item-id');
|
||||
button.disabled = true;
|
||||
|
||||
fetch(`/admin/sync_suggestion/${itemId}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
showToast(data.message, data.success ? 'success' : 'danger');
|
||||
|
||||
if (data.success) {
|
||||
button.innerText = '✅ Zsynchronizowano';
|
||||
button.classList.remove('btn-outline-primary');
|
||||
button.classList.add('btn-success');
|
||||
} else {
|
||||
button.disabled = false;
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
showToast('Błąd synchronizacji', 'danger');
|
||||
button.disabled = false;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function bindDeleteButton(button) {
|
||||
button.addEventListener('click', function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
const suggestionId = button.getAttribute('data-suggestion-id');
|
||||
const row = button.closest('tr');
|
||||
const itemId = button.getAttribute('data-item-id');
|
||||
const nameBadge = row?.querySelector('.badge.bg-primary');
|
||||
const itemName = nameBadge?.innerText.trim().toLowerCase();
|
||||
|
||||
button.disabled = true;
|
||||
|
||||
fetch(`/admin/delete_suggestion/${suggestionId}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
showToast(data.message, data.success ? 'success' : 'danger');
|
||||
|
||||
if (!data.success || !row) {
|
||||
button.disabled = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const isProductRow = typeof itemId === 'string' && itemId !== '';
|
||||
const cell = row.querySelector('td:last-child');
|
||||
if (!cell) return;
|
||||
|
||||
if (isProductRow) {
|
||||
cell.innerHTML = `<button class="btn btn-sm btn-outline-light sync-btn" data-item-id="${itemId}">🔄 Synchronizuj</button>`;
|
||||
const syncBtn = cell.querySelector('.sync-btn');
|
||||
if (syncBtn) bindSyncButton(syncBtn);
|
||||
} else {
|
||||
cell.innerHTML = '<span class="badge rounded-pill bg-warning opacity-75">Usunięto synchronizacje</span>';
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
showToast('Błąd usuwania sugestii', 'danger');
|
||||
button.disabled = false;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
document.querySelectorAll('.sync-btn').forEach(btn => {
|
||||
btn.replaceWith(btn.cloneNode(true));
|
||||
});
|
||||
document.querySelectorAll('.delete-suggestion-btn').forEach(btn => {
|
||||
btn.replaceWith(btn.cloneNode(true));
|
||||
});
|
||||
|
||||
document.querySelectorAll('.sync-btn').forEach(btn => {
|
||||
btn.addEventListener('click', function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
const itemId = this.getAttribute('data-item-id');
|
||||
const button = this;
|
||||
button.disabled = true;
|
||||
|
||||
fetch(`/admin/sync_suggestion/${itemId}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
showToast(data.message, data.success ? 'success' : 'danger');
|
||||
|
||||
if (data.success) {
|
||||
button.innerText = '✅ Zsynchronizowano';
|
||||
button.classList.remove('btn-outline-primary');
|
||||
button.classList.add('btn-success');
|
||||
} else {
|
||||
button.disabled = false;
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
showToast('Błąd synchronizacji', 'danger');
|
||||
button.disabled = false;
|
||||
});
|
||||
});
|
||||
const clone = btn.cloneNode(true);
|
||||
btn.replaceWith(clone);
|
||||
bindSyncButton(clone);
|
||||
});
|
||||
|
||||
document.querySelectorAll('.delete-suggestion-btn').forEach(btn => {
|
||||
btn.addEventListener('click', function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
const suggestionId = this.getAttribute('data-suggestion-id');
|
||||
const button = this;
|
||||
button.disabled = true;
|
||||
|
||||
fetch(`/admin/delete_suggestion/${suggestionId}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
showToast(data.message, data.success ? 'success' : 'danger');
|
||||
|
||||
if (data.success) {
|
||||
const row = button.closest('tr');
|
||||
if (row) row.remove();
|
||||
} else {
|
||||
button.disabled = false;
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
showToast('Błąd usuwania sugestii', 'danger');
|
||||
button.disabled = false;
|
||||
});
|
||||
});
|
||||
const clone = btn.cloneNode(true);
|
||||
btn.replaceWith(clone);
|
||||
bindDeleteButton(clone);
|
||||
});
|
||||
});
|
||||
|
@@ -224,11 +224,11 @@
|
||||
|
||||
<td>
|
||||
{% if l.is_archived %}
|
||||
<span class="badge bg-secondary">Archiwalna</span>
|
||||
<span class="badge rounded-pill bg-secondary">Archiwalna</span>
|
||||
{% elif e.expired %}
|
||||
<span class="badge bg-warning text-dark">Wygasła</span>
|
||||
<span class="badge rounded-pill bg-warning text-dark">Wygasła</span>
|
||||
{% else %}
|
||||
<span class="badge bg-success">Aktywna</span>
|
||||
<span class="badge rounded-pill bg-success">Aktywna</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ l.created_at.strftime('%Y-%m-%d %H:%M') if l.created_at else '-' }}</td>
|
||||
@@ -250,8 +250,8 @@
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td><span class="badge bg-primary">{{ e.comments_count }}</span></td>
|
||||
<td><span class="badge bg-secondary">{{ e.receipts_count }}</span></td>
|
||||
<td><span class="badge rounded-pill bg-primary">{{ e.comments_count }}</span></td>
|
||||
<td><span class="badge rounded-pill bg-secondary">{{ e.receipts_count }}</span></td>
|
||||
<td class="fw-bold
|
||||
{% if e.total_expense >= 500 %}text-danger
|
||||
{% elif e.total_expense > 0 %}text-success{% endif %}">
|
||||
|
@@ -11,7 +11,7 @@
|
||||
<div class="card-body">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h4 class="m-0">📦 Produkty (z synchronizacją sugestii o unikalnych nazwach)</h4>
|
||||
<span class="badge bg-info">{{ total_items }} produktów</span>
|
||||
<span class="badge rounded-pill bg-info">{{ total_items }} produktów</span>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<table class="table table-dark table-striped align-middle sortable">
|
||||
@@ -28,7 +28,7 @@
|
||||
{% for item in items %}
|
||||
<tr>
|
||||
<td>{{ item.id }}</td>
|
||||
<td class="fw-bold"><span class="badge bg-primary">{{ item.name }}</span></td>
|
||||
<td class="fw-bold"><span class="badge rounded-pill bg-primary">{{ item.name }}</span></td>
|
||||
<td>
|
||||
{% if item.added_by and users_dict.get(item.added_by) %}
|
||||
👤 {{ users_dict[item.added_by] }} ({{ item.added_by }})
|
||||
@@ -36,15 +36,16 @@
|
||||
-
|
||||
{% endif %}
|
||||
</td>
|
||||
<td><span class="badge bg-secondary">{{ usage_counts.get(item.name.lower(), 0) }}</span></td>
|
||||
<td><span class="badge rounded-pill bg-secondary">{{ usage_counts.get(item.name.lower(), 0) }}</span></td>
|
||||
<td>
|
||||
{% set suggestion = suggestions_dict.get(item.name.lower()) %}
|
||||
{% set clean_name = item.name | replace('\xa0', ' ') | trim | lower %}
|
||||
{% set suggestion = suggestions_dict.get(clean_name) %}
|
||||
{% if suggestion %}
|
||||
✅ Istnieje (ID: {{ suggestion.id }})
|
||||
<button class="btn btn-sm btn-outline-danger ms-1 delete-suggestion-btn"
|
||||
<button class="btn btn-sm btn-outline-light ms-1 delete-suggestion-btn"
|
||||
data-suggestion-id="{{ suggestion.id }}">🗑️ Usuń</button>
|
||||
{% else %}
|
||||
<button class="btn btn-sm btn-outline-primary sync-btn" data-item-id="{{ item.id }}">🔄
|
||||
<button class="btn btn-sm btn-outline-light sync-btn" data-item-id="{{ item.id }}">🔄
|
||||
Synchronizuj</button>
|
||||
{% endif %}
|
||||
</td>
|
||||
@@ -65,7 +66,7 @@
|
||||
<div class="card-body">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h4 class="m-0">💡 Wszystkie sugestie (poza powiązanymi)</h4>
|
||||
<span class="badge bg-info">{{ orphan_suggestions|length }} sugestii</span>
|
||||
<span class="badge rounded-pill bg-info">{{ orphan_suggestions|length }} sugestii</span>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
{% set item_names = items | map(attribute='name') | map('lower') | list %}
|
||||
@@ -82,9 +83,9 @@
|
||||
{% if suggestion.name.lower() not in item_names %}
|
||||
<tr>
|
||||
<td>{{ suggestion.id }}</td>
|
||||
<td class="fw-bold"><span class="badge bg-primary">{{ suggestion.name }}</span></td>
|
||||
<td class="fw-bold"><span class="badge rounded-pill bg-primary">{{ suggestion.name }}</span></td>
|
||||
<td>
|
||||
<button class="btn btn-sm btn-outline-danger delete-suggestion-btn"
|
||||
<button class="btn btn-sm btn-outline-light delete-suggestion-btn"
|
||||
data-suggestion-id="{{ suggestion.id }}">🗑️ Usuń</button>
|
||||
</td>
|
||||
</tr>
|
||||
|
@@ -46,11 +46,14 @@
|
||||
</td>
|
||||
<td>{{ l.created_at.strftime('%Y-%m-%d %H:%M') if l.created_at else '-' }}</td>
|
||||
<td>
|
||||
{% if l.is_archived %}<span class="badge bg-secondary">Archiwalna</span>{% endif %}
|
||||
{% if l.is_temporary %}<span class="badge bg-warning text-dark">Tymczasowa</span>{%
|
||||
{% if l.is_archived %}<span class="badge rounded-pill bg-secondary">Archiwalna</span>{%
|
||||
endif %}
|
||||
{% if l.is_public %}<span class="badge bg-success">Publiczna</span>{% else %}
|
||||
<span class="badge bg-dark">Prywatna</span>{% endif %}
|
||||
{% if l.is_temporary %}<span
|
||||
class="badge rounded-pill bg-warning text-dark">Tymczasowa</span>{%
|
||||
endif %}
|
||||
{% if l.is_public %}<span class="badge rounded-pill bg-success">Publiczna</span>{% else
|
||||
%}
|
||||
<span class="badge rounded-pill bg-dark">Prywatna</span>{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<button type="button" class="btn btn-sm btn-outline-info preview-btn"
|
||||
|
@@ -47,9 +47,9 @@
|
||||
<td class="fw-bold">{{ user.username }}</td>
|
||||
<td>
|
||||
{% if user.is_admin %}
|
||||
<span class="badge bg-primary">Admin</span>
|
||||
<span class="badge rounded-pill bg-primary">Admin</span>
|
||||
{% else %}
|
||||
<span class="badge bg-secondary">Użytkownik</span>
|
||||
<span class="badge rounded-pill bg-secondary">Użytkownik</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
|
@@ -42,12 +42,12 @@
|
||||
{% if current_user.is_authenticated %}
|
||||
<div class="d-flex justify-content-center align-items-center text-white small flex-wrap text-center">
|
||||
<span class="me-1">Zalogowany:</span>
|
||||
<span class="badge bg-success">{{ current_user.username }}</span>
|
||||
<span class="badge rounded-pill bg-success">{{ current_user.username }}</span>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="d-flex justify-content-center align-items-center text-white small flex-wrap text-center">
|
||||
<span class="me-1">Przeglądasz jako</span>
|
||||
<span class="badge bg-info">gość</span>
|
||||
<span class="badge rounded-pill bg-info">gość</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
@@ -60,7 +60,7 @@
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">📆 Utworzono:</label>
|
||||
<p class="form-control-plaintext text-white">
|
||||
<span class="badge bg-success rounded-pill text-dark ms-1">
|
||||
<span class="badge rounded-pill bg-success rounded-pill text-dark ms-1">
|
||||
{{ list.created_at.strftime('%Y-%m-%d') }}
|
||||
</span>
|
||||
</p>
|
||||
|
@@ -6,12 +6,12 @@
|
||||
<h2 class="mb-2">
|
||||
Lista: <strong>{{ list.title }}</strong>
|
||||
{% if list.is_archived %}
|
||||
<span class="badge bg-secondary ms-2">(Archiwalna)</span>
|
||||
<span class="badge rounded-pill bg-secondary ms-2">(Archiwalna)</span>
|
||||
{% endif %}
|
||||
|
||||
{% if list.category_badges %}
|
||||
{% for cat in list.category_badges %}
|
||||
<span class="badge rounded-pill text-dark ms-1" style="background-color: {{ cat.color }};
|
||||
<span class="badge rounded-pill rounded-pill text-dark ms-1" style="background-color: {{ cat.color }};
|
||||
font-size: 0.75rem;
|
||||
opacity: 0.85;">
|
||||
{{ cat.name }}
|
||||
@@ -41,7 +41,7 @@
|
||||
🙈 Lista jest ukryta przed gośćmi
|
||||
{% endif %}
|
||||
</strong>
|
||||
<span id="share-url" class="badge bg-secondary text-wrap"
|
||||
<span id="share-url" class="badge rounded-pill bg-secondary text-wrap"
|
||||
style="font-size: 0.7rem; {% if not list.is_public %}display: none;{% endif %}">
|
||||
{{ request.url_root }}share/{{ list.share_token }}
|
||||
</span>
|
||||
@@ -120,7 +120,7 @@
|
||||
<span id="name-{{ item.id }}" class="text-white">
|
||||
{{ item.name }}
|
||||
{% if item.quantity and item.quantity > 1 %}
|
||||
<span class="badge bg-secondary">x{{ item.quantity }}</span>
|
||||
<span class="badge rounded-pill bg-secondary">x{{ item.quantity }}</span>
|
||||
{% endif %}
|
||||
</span>
|
||||
|
||||
|
@@ -6,15 +6,15 @@
|
||||
🛍️ {{ list.title }}
|
||||
|
||||
{% if list.is_archived %}
|
||||
<span class="badge bg-secondary ms-2">(Archiwalna)</span>
|
||||
<span class="badge rounded-pill bg-secondary ms-2">(Archiwalna)</span>
|
||||
{% endif %}
|
||||
|
||||
{% if total_expense > 0 %}
|
||||
<span id="total-expense1" class="badge bg-success ms-2">
|
||||
<span id="total-expense1" class="badge rounded-pill bg-success ms-2">
|
||||
💸 {{ '%.2f'|format(total_expense) }} PLN
|
||||
</span>
|
||||
{% else %}
|
||||
<span id="total-expense" class="badge bg-secondary ms-2" style="display: none;">
|
||||
<span id="total-expense" class="badge rounded-pill bg-secondary ms-2" style="display: none;">
|
||||
💸 0.00 PLN
|
||||
</span>
|
||||
{% endif %}
|
||||
@@ -22,7 +22,7 @@
|
||||
{# Kategorie - tylko wyświetlenie, bez linków #}
|
||||
{% if list.category_badges %}
|
||||
{% for cat in list.category_badges %}
|
||||
<span class="badge rounded-pill text-dark ms-1" style="background-color: {{ cat.color }};
|
||||
<span class="badge rounded-pill rounded-pill text-dark ms-1" style="background-color: {{ cat.color }};
|
||||
font-size: 0.75rem;
|
||||
opacity: 0.85;">
|
||||
{{ cat.name }}
|
||||
@@ -53,7 +53,7 @@
|
||||
<span id="name-{{ item.id }}" class="text-white">
|
||||
{{ item.name }}
|
||||
{% if item.quantity and item.quantity > 1 %}
|
||||
<span class="badge bg-secondary">x{{ item.quantity }}</span>
|
||||
<span class="badge rounded-pill bg-secondary">x{{ item.quantity }}</span>
|
||||
{% endif %}
|
||||
</span>
|
||||
|
||||
|
Reference in New Issue
Block a user