Compare commits
15 Commits
ac88869f52
...
master
Author | SHA1 | Date | |
---|---|---|---|
![]() |
68f235d605 | ||
![]() |
ea46dd43e1 | ||
![]() |
4b99b109bd | ||
![]() |
028ae3c26e | ||
![]() |
71b14411e5 | ||
![]() |
f1744fae99 | ||
![]() |
79c6f7d0b1 | ||
![]() |
80651bc3c7 | ||
![]() |
4602fb7749 | ||
![]() |
40381774b4 | ||
![]() |
cc988d5934 | ||
![]() |
883562c532 | ||
![]() |
5e01a735d3 | ||
![]() |
4988ad9a5f | ||
![]() |
d321521ef1 |
108
app.py
108
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 ###########################
|
||||
|
||||
|
||||
@@ -1527,20 +1533,18 @@ def edit_my_list(list_id):
|
||||
next_page = request.args.get("next") or request.referrer
|
||||
|
||||
if request.method == "POST":
|
||||
move_to_month = request.form.get("move_to_month", "")
|
||||
|
||||
move_to_month = request.form.get("move_to_month")
|
||||
if move_to_month:
|
||||
try:
|
||||
year, month = map(int, move_to_month.split("-"))
|
||||
new_created_at = datetime(year, month, 1, tzinfo=timezone.utc)
|
||||
if l.created_at is None or l.created_at.year != year or l.created_at.month != month:
|
||||
l.created_at = new_created_at
|
||||
db.session.commit()
|
||||
flash(
|
||||
f"Zmieniono datę utworzenia listy na {new_created_at.strftime('%Y-%m-%d')}",
|
||||
"success",
|
||||
)
|
||||
return redirect(next_page or url_for("main_page"))
|
||||
l.created_at = new_created_at
|
||||
db.session.commit()
|
||||
flash(
|
||||
f"Zmieniono datę utworzenia listy na {new_created_at.strftime('%Y-%m-%d')}",
|
||||
"success",
|
||||
)
|
||||
return redirect(next_page or url_for("main_page"))
|
||||
except ValueError:
|
||||
flash("Nieprawidłowy format miesiąca", "danger")
|
||||
return redirect(next_page or url_for("main_page"))
|
||||
@@ -1577,26 +1581,15 @@ def edit_my_list(list_id):
|
||||
flash("Zaktualizowano dane listy", "success")
|
||||
return redirect(next_page or url_for("main_page"))
|
||||
|
||||
if l.created_at:
|
||||
selected_year = l.created_at.year
|
||||
selected_month = f"{l.created_at.month:02d}"
|
||||
else:
|
||||
now = datetime.now()
|
||||
selected_year = now.year
|
||||
selected_month = f"{now.month:02d}"
|
||||
|
||||
return render_template(
|
||||
"edit_my_list.html",
|
||||
list=l,
|
||||
receipts=receipts,
|
||||
categories=categories,
|
||||
selected_categories=selected_categories_ids,
|
||||
current_year=selected_year,
|
||||
current_month=selected_month,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@app.route("/delete_user_list/<int:list_id>", methods=["POST"])
|
||||
@login_required
|
||||
def delete_user_list(list_id):
|
||||
@@ -2469,9 +2462,8 @@ def admin_receipts(id):
|
||||
if id == "all":
|
||||
all_filenames = {r.filename for r in Receipt.query.all()}
|
||||
|
||||
pagination = (
|
||||
Receipt.query.order_by(Receipt.uploaded_at.desc())
|
||||
.paginate(page=page, per_page=per_page, error_out=False)
|
||||
pagination = Receipt.query.order_by(Receipt.uploaded_at.desc()).paginate(
|
||||
page=page, per_page=per_page, error_out=False
|
||||
)
|
||||
|
||||
receipts_paginated = pagination.items
|
||||
@@ -2849,56 +2841,66 @@ def edit_list(list_id):
|
||||
@admin_required
|
||||
def list_products():
|
||||
page = request.args.get("page", 1, type=int)
|
||||
per_page = request.args.get("per_page", 125, type=int)
|
||||
per_page = request.args.get("per_page", 100, type=int)
|
||||
per_page = max(1, min(per_page, 300))
|
||||
|
||||
# Pobierz wszystkie itemy – tylko name, id i relacje potrzebne do odtworzenia unikalnych
|
||||
all_items = Item.query.options(
|
||||
joinedload(Item.shopping_list),
|
||||
joinedload(Item.added_by_user)
|
||||
).order_by(Item.id.desc()).all()
|
||||
all_items = (
|
||||
Item.query.options(
|
||||
joinedload(Item.added_by_user),
|
||||
)
|
||||
.order_by(Item.id.desc())
|
||||
.all()
|
||||
)
|
||||
|
||||
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)
|
||||
|
||||
# Paginacja ręczna na unikalnych
|
||||
usage_counts = dict(
|
||||
db.session.query(
|
||||
func.lower(Item.name),
|
||||
func.coalesce(func.sum(Item.quantity), 0)
|
||||
)
|
||||
.group_by(func.lower(Item.name))
|
||||
.all()
|
||||
)
|
||||
|
||||
total_items = len(unique_items)
|
||||
total_pages = (total_items + per_page - 1) // per_page
|
||||
start = (page - 1) * per_page
|
||||
end = start + per_page
|
||||
items = unique_items[start:end]
|
||||
|
||||
# Słownik użytkowników (ograniczony do używanych)
|
||||
user_ids = {item.added_by for item in items if item.added_by}
|
||||
users = User.query.filter(User.id.in_(user_ids)).all()
|
||||
users = User.query.filter(User.id.in_(user_ids)).all() if user_ids else []
|
||||
users_dict = {u.id: u.username for u in users}
|
||||
|
||||
# Wszystkie sugestie – do dopasowań
|
||||
suggestions = SuggestedProduct.query.all()
|
||||
|
||||
all_suggestions_dict = {
|
||||
(s.name or "").strip().lower(): s for s in suggestions if s.name and s.name.strip()
|
||||
normalize_name(s.name): s
|
||||
for s in suggestions
|
||||
if s.name and s.name.strip()
|
||||
}
|
||||
used_suggestion_names = {normalize_name(i.name) for i in unique_items}
|
||||
|
||||
used_suggestion_names = {(item.name or "").strip().lower() for item in unique_items}
|
||||
|
||||
# Powiązane i osierocone
|
||||
suggestions_dict = {
|
||||
name: all_suggestions_dict[name]
|
||||
for name in used_suggestion_names
|
||||
if name in all_suggestions_dict
|
||||
}
|
||||
|
||||
orphan_suggestions = [
|
||||
s for name, s in all_suggestions_dict.items()
|
||||
if name not in used_suggestion_names
|
||||
]
|
||||
|
||||
|
||||
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",
|
||||
@@ -2910,9 +2912,13 @@ def list_products():
|
||||
per_page=per_page,
|
||||
total_pages=total_pages,
|
||||
query_string=query_string,
|
||||
total_items=total_items,
|
||||
usage_counts=usage_counts,
|
||||
synced_names=synced_names
|
||||
)
|
||||
|
||||
|
||||
|
||||
@app.route("/admin/sync_suggestion/<int:item_id>", methods=["POST"])
|
||||
@login_required
|
||||
def sync_suggestion_ajax(item_id):
|
||||
@@ -3013,7 +3019,6 @@ def recalculate_filesizes_all():
|
||||
return redirect(url_for("admin_receipts", id="all"))
|
||||
|
||||
|
||||
|
||||
@app.route("/admin/mass_edit_categories", methods=["GET", "POST"])
|
||||
@login_required
|
||||
@admin_required
|
||||
@@ -3047,7 +3052,9 @@ def admin_mass_edit_categories():
|
||||
l.categories.extend(cats)
|
||||
db.session.commit()
|
||||
flash("Zaktualizowano kategorie dla wybranych list", "success")
|
||||
return redirect(url_for("admin_mass_edit_categories", page=page, per_page=per_page))
|
||||
return redirect(
|
||||
url_for("admin_mass_edit_categories", page=page, per_page=per_page)
|
||||
)
|
||||
|
||||
query_string = urlencode({k: v for k, v in request.args.items() if k != "page"})
|
||||
|
||||
@@ -3084,14 +3091,15 @@ def admin_list_items_json(list_id):
|
||||
purchased_count = sum(1 for item in l.items if item.purchased)
|
||||
total_expense = sum(exp.amount for exp in l.expenses)
|
||||
|
||||
return jsonify({
|
||||
"title": l.title,
|
||||
"items": items,
|
||||
"total_count": len(l.items),
|
||||
"purchased_count": purchased_count,
|
||||
"total_expense": round(total_expense, 2)
|
||||
})
|
||||
|
||||
return jsonify(
|
||||
{
|
||||
"title": l.title,
|
||||
"items": items,
|
||||
"total_count": len(l.items),
|
||||
"purchased_count": purchased_count,
|
||||
"total_expense": round(total_expense, 2),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@app.route("/healthcheck")
|
||||
|
@@ -1,109 +0,0 @@
|
||||
let cropper;
|
||||
let currentReceiptId;
|
||||
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
const cropModal = document.getElementById("cropModal");
|
||||
const cropImage = document.getElementById("cropImage");
|
||||
const spinner = document.getElementById("cropLoading");
|
||||
|
||||
cropModal.addEventListener("shown.bs.modal", function (event) {
|
||||
const button = event.relatedTarget;
|
||||
const imgSrc = button.getAttribute("data-img-src");
|
||||
currentReceiptId = button.getAttribute("data-receipt-id");
|
||||
|
||||
cropImage.src = imgSrc;
|
||||
|
||||
if (cropper) {
|
||||
cropper.destroy();
|
||||
cropper = null;
|
||||
}
|
||||
|
||||
cropImage.onload = () => {
|
||||
cropper = new Cropper(cropImage, {
|
||||
viewMode: 1,
|
||||
autoCropArea: 1,
|
||||
responsive: true,
|
||||
background: false,
|
||||
zoomable: true,
|
||||
movable: true,
|
||||
dragMode: 'move',
|
||||
minContainerHeight: 400,
|
||||
minContainerWidth: 400,
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
document.getElementById("saveCrop").addEventListener("click", function () {
|
||||
if (!cropper) return;
|
||||
|
||||
spinner.classList.remove("d-none");
|
||||
|
||||
const cropData = cropper.getData();
|
||||
const imageData = cropper.getImageData();
|
||||
|
||||
const scaleX = imageData.naturalWidth / imageData.width;
|
||||
const scaleY = imageData.naturalHeight / imageData.height;
|
||||
|
||||
const width = cropData.width * scaleX;
|
||||
const height = cropData.height * scaleY;
|
||||
|
||||
if (width < 1 || height < 1) {
|
||||
spinner.classList.add("d-none");
|
||||
showToast("Obszar przycięcia jest zbyt mały lub pusty", "danger");
|
||||
return;
|
||||
}
|
||||
|
||||
// Ogranicz do 2000x2000 w proporcji
|
||||
const maxDim = 2000;
|
||||
const scale = Math.min(1, maxDim / Math.max(width, height));
|
||||
|
||||
const finalWidth = Math.round(width * scale);
|
||||
const finalHeight = Math.round(height * scale);
|
||||
|
||||
const croppedCanvas = cropper.getCroppedCanvas({
|
||||
width: finalWidth,
|
||||
height: finalHeight,
|
||||
imageSmoothingEnabled: true,
|
||||
imageSmoothingQuality: 'high',
|
||||
});
|
||||
|
||||
|
||||
if (!croppedCanvas) {
|
||||
spinner.classList.add("d-none");
|
||||
showToast("Nie można uzyskać obrazu przycięcia", "danger");
|
||||
return;
|
||||
}
|
||||
|
||||
croppedCanvas.toBlob(function (blob) {
|
||||
if (!blob) {
|
||||
spinner.classList.add("d-none");
|
||||
showToast("Nie udało się zapisać obrazu", "danger");
|
||||
return;
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append("receipt_id", currentReceiptId);
|
||||
formData.append("cropped_image", blob);
|
||||
|
||||
fetch("/admin/crop_receipt", {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
spinner.classList.add("d-none");
|
||||
if (data.success) {
|
||||
showToast("Zapisano przycięty paragon", "success");
|
||||
setTimeout(() => location.reload(), 1500);
|
||||
} else {
|
||||
showToast("Błąd: " + (data.error || "Nieznany"), "danger");
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
spinner.classList.add("d-none");
|
||||
showToast("Błąd sieci", "danger");
|
||||
console.error(err);
|
||||
});
|
||||
}, "image/webp", 1.0);
|
||||
});
|
||||
});
|
@@ -1,11 +0,0 @@
|
||||
function updateMoveToMonth() {
|
||||
const year = document.getElementById('move_to_month_year').value;
|
||||
const month = document.getElementById('move_to_month_month').value;
|
||||
const hiddenInput = document.getElementById('move_to_month');
|
||||
hiddenInput.value = `${year}-${month}`;
|
||||
}
|
||||
|
||||
document.getElementById('move_to_month_year').addEventListener('change', updateMoveToMonth);
|
||||
document.getElementById('move_to_month_month').addEventListener('change', updateMoveToMonth);
|
||||
|
||||
updateMoveToMonth();
|
@@ -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 %}">
|
||||
|
@@ -88,7 +88,7 @@
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label for="created_month" class="form-label">📁 Przenieś do miesiąca</label>
|
||||
<label class="form-label">📁 Przenieś do miesiąca (format: rok-miesiąc np 2026-01)</label>
|
||||
<input type="month" id="created_month" name="created_month"
|
||||
class="form-control bg-dark text-white border-secondary rounded">
|
||||
</div>
|
||||
@@ -282,5 +282,4 @@
|
||||
{% endblock %}
|
||||
{% block scripts %}
|
||||
<script src="{{ url_for('static_bp.serve_js', filename='select.js') }}"></script>
|
||||
<script src="{{ url_for('static_bp.serve_js', filename='move_to_month.js') }}"></script>
|
||||
{% endblock %}
|
@@ -9,11 +9,9 @@
|
||||
|
||||
<div class="card bg-dark text-white mb-5">
|
||||
<div class="card-body">
|
||||
|
||||
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h4 class="m-0">📦 Produkty (z synchronizacją sugestii)</h4>
|
||||
<span class="badge bg-info">{{ items|length }} produktów</span>
|
||||
<h4 class="m-0">📦 Produkty (z synchronizacją sugestii o unikalnych nazwach)</h4>
|
||||
<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">
|
||||
@@ -22,15 +20,15 @@
|
||||
<th>ID</th>
|
||||
<th>Nazwa</th>
|
||||
<th>Dodany przez</th>
|
||||
<th>Sugestia</th>
|
||||
<th>Lista</th>
|
||||
<th>Ilość użyć</th>
|
||||
<th>Akcja</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% 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 }})
|
||||
@@ -38,37 +36,24 @@
|
||||
-
|
||||
{% endif %}
|
||||
</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>
|
||||
<td>
|
||||
<div class="btn-group btn-group-sm" role="group">
|
||||
<button type="button" class="btn btn-outline-light disabled" data-bs-toggle="tooltip"
|
||||
data-bs-title="{{ item.shopping_list.title }}">
|
||||
ID: {{ item.list_id }}
|
||||
</button>
|
||||
<a href="{{ url_for('view_list', list_id=item.list_id) }}" class="btn btn-outline-light">
|
||||
📄 Otwórz
|
||||
</a>
|
||||
<button type="button" class="btn btn-outline-light preview-btn" data-list-id="{{ item.list_id }}">
|
||||
🔍 Podgląd
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% if items|length == 0 %}
|
||||
<tr>
|
||||
<td colspan="5" class="text-center text-muted">Brak produktów do wyświetlenia.</td>
|
||||
<td colspan="5" class="text-center">Pusta lista produktów.</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</tbody>
|
||||
@@ -81,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 %}
|
||||
@@ -98,17 +83,17 @@
|
||||
{% 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>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% if suggestions_dict|length == 0 %}
|
||||
{% if orphan_suggestions|length == 0 %}
|
||||
<tr>
|
||||
<td colspan="3" class="text-center text-muted">Brak sugestii do wyświetlenia.</td>
|
||||
<td colspan="3" class="text-center">Brak sugestii do wyświetlenia.</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</tbody>
|
||||
@@ -121,9 +106,9 @@
|
||||
<form method="get" class="d-flex align-items-center">
|
||||
<label for="per_page" class="me-2">🔢 Pozycji na stronę:</label>
|
||||
<select id="per_page" name="per_page" class="form-select form-select-sm me-2" onchange="this.form.submit()">
|
||||
<option value="25" {% if per_page==100 %}selected{% endif %}>100</option>
|
||||
<option value="50" {% if per_page==200 %}selected{% endif %}>200</option>
|
||||
<option value="100" {% if per_page==300 %}selected{% endif %}>300</option>
|
||||
<option value="100" {% if per_page==100 %}selected{% endif %}>100</option>
|
||||
<option value="200" {% if per_page==200 %}selected{% endif %}>200</option>
|
||||
<option value="300" {% if per_page==300 %}selected{% endif %}>300</option>
|
||||
</select>
|
||||
<input type="hidden" name="page" value="{{ page }}">
|
||||
</form>
|
||||
@@ -145,24 +130,8 @@
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<!-- Modal podglądu produktów -->
|
||||
<div class="modal fade" id="productPreviewModal" tabindex="-1" aria-labelledby="previewModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg modal-dialog-scrollable">
|
||||
<div class="modal-content bg-dark text-white">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="previewModalLabel">Podgląd produktów</h5>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Zamknij"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<ul id="product-list" class="list-group list-group-flush"></ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% block scripts %}
|
||||
<script src="{{ url_for('static_bp.serve_js', filename='product_suggestion.js') }}"></script>
|
||||
<script src="{{ url_for('static_bp.serve_js', filename='preview_list_modal.js') }}"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% endblock %}
|
@@ -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>
|
||||
|
@@ -8,26 +8,26 @@
|
||||
<link rel="icon" type="image/svg+xml" href="{{ url_for('favicon') }}">
|
||||
|
||||
{# --- Style CSS ładowane tylko dla niezablokowanych --- #}
|
||||
{% if not is_blocked %}
|
||||
{% if not is_blocked %}
|
||||
<link href="{{ url_for('static_bp.serve_css', filename='style.css') }}" rel="stylesheet">
|
||||
<link href="{{ url_for('static_bp.serve_css_lib', filename='glightbox.min.css') }}" rel="stylesheet">
|
||||
<link href="{{ url_for('static_bp.serve_css_lib', filename='sort_table.min.css') }}" rel="stylesheet">
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{# --- Bootstrap zawsze --- #}
|
||||
<link href="{{ url_for('static_bp.serve_css_lib', filename='bootstrap.min.css') }}" rel="stylesheet">
|
||||
{# --- Bootstrap zawsze --- #}
|
||||
<link href="{{ url_for('static_bp.serve_css_lib', filename='bootstrap.min.css') }}" rel="stylesheet">
|
||||
|
||||
{# --- Cropper CSS tylko dla wybranych podstron --- #}
|
||||
{% set substrings_cropper = ['/admin/receipts', '/edit_my_list'] %}
|
||||
{% if substrings_cropper | select("in", request.path) | list | length > 0 %}
|
||||
{# --- Cropper CSS tylko dla wybranych podstron --- #}
|
||||
{% set substrings_cropper = ['/admin/receipts', '/edit_my_list'] %}
|
||||
{% if substrings_cropper | select("in", request.path) | list | length > 0 %}
|
||||
<link href="{{ url_for('static_bp.serve_css_lib', filename='cropper.min.css') }}" rel="stylesheet">
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{# --- Tom Select CSS tylko dla wybranych podstron --- #}
|
||||
{% set substrings_tomselect = ['/edit_my_list', '/admin/edit_list', '/admin/mass_edit_categories'] %}
|
||||
{% if substrings_tomselect | select("in", request.path) | list | length > 0 %}
|
||||
{# --- Tom Select CSS tylko dla wybranych podstron --- #}
|
||||
{% set substrings_tomselect = ['/edit_my_list', '/admin/edit_list', '/admin/mass_edit_categories'] %}
|
||||
{% if substrings_tomselect | select("in", request.path) | list | length > 0 %}
|
||||
<link href="{{ url_for('static_bp.serve_css_lib', filename='tom-select.bootstrap5.min.css') }}" rel="stylesheet">
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</head>
|
||||
|
||||
<body class="bg-dark text-white">
|
||||
@@ -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 %}
|
||||
|
@@ -22,20 +22,20 @@
|
||||
<label class="form-label">⚙️ Statusy listy</label>
|
||||
<div class="d-flex flex-wrap gap-3">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" id="public" name="is_public"
|
||||
{% if list.is_public %}checked{% endif %}>
|
||||
<input class="form-check-input" type="checkbox" id="public" name="is_public" {% if list.is_public
|
||||
%}checked{% endif %}>
|
||||
<label class="form-check-label" for="public">🌐 Publiczna</label>
|
||||
</div>
|
||||
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" id="temporary" name="is_temporary"
|
||||
{% if list.is_temporary %}checked{% endif %}>
|
||||
<input class="form-check-input" type="checkbox" id="temporary" name="is_temporary" {% if list.is_temporary
|
||||
%}checked{% endif %}>
|
||||
<label class="form-check-label" for="temporary">⏳ Tymczasowa</label>
|
||||
</div>
|
||||
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" id="archived" name="is_archived"
|
||||
{% if list.is_archived %}checked{% endif %}>
|
||||
<input class="form-check-input" type="checkbox" id="archived" name="is_archived" {% if list.is_archived
|
||||
%}checked{% endif %}>
|
||||
<label class="form-check-label" for="archived">📦 Archiwalna</label>
|
||||
</div>
|
||||
</div>
|
||||
@@ -55,32 +55,20 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-4">
|
||||
<!-- Utworzono / Zmień miesiąc -->
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">📆 Utworzono</label>
|
||||
<span class="badge bg-success rounded-pill text-dark ms-1">
|
||||
{{ list.created_at.strftime('%Y-%m-%d') }}
|
||||
</span>
|
||||
<label class="form-label">📆 Utworzono:</label>
|
||||
<p class="form-control-plaintext text-white">
|
||||
<span class="badge rounded-pill bg-success rounded-pill text-dark ms-1">
|
||||
{{ list.created_at.strftime('%Y-%m-%d') }}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">📁 Przenieś do miesiąca</label>
|
||||
<div class="d-flex gap-2">
|
||||
<select id="move_to_month_year" class="form-select bg-dark text-white border-secondary rounded">
|
||||
{% for y in range(2022, 2031) %}
|
||||
<option value="{{ y }}" {% if y == current_year %}selected{% endif %}>{{ y }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<select id="move_to_month_month" class="form-select bg-dark text-white border-secondary rounded">
|
||||
{% for m in range(1, 13) %}
|
||||
<option value="{{ "%02d"|format(m) }}" {% if "%02d"|format(m) == current_month %}selected{% endif %}>
|
||||
{{ "%02d"|format(m) }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Ukryte pole, które jest wysyłane do backendu -->
|
||||
<input type="hidden" id="move_to_month" name="move_to_month" value="{{ list.move_to_month or '' }}">
|
||||
<label class="form-label">📁 Przenieś do miesiąca (format: rok-miesiąc np 2026-01)</label>
|
||||
<input type="month" id="move_to_month" name="move_to_month"
|
||||
class="form-control bg-dark text-white border-secondary rounded">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -108,55 +96,55 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if receipts %}
|
||||
<hr class="my-4">
|
||||
<h5>Paragony przypisane do tej listy</h5>
|
||||
{% if receipts %}
|
||||
<hr class="my-4">
|
||||
<h5>Paragony przypisane do tej listy</h5>
|
||||
|
||||
<div class="row">
|
||||
{% for r in receipts %}
|
||||
<div class="col-6 col-md-4 col-lg-3">
|
||||
<div class="card bg-dark text-white h-100">
|
||||
<a href="{{ url_for('uploaded_file', filename=r.filename) }}" class="glightbox" data-gallery="receipts"
|
||||
data-title="{{ r.filename }}">
|
||||
<img src="{{ url_for('uploaded_file', filename=r.filename) }}" class="card-img-top"
|
||||
style="object-fit: cover; height: 200px;">
|
||||
</a>
|
||||
<div class="card-body text-center">
|
||||
<p class="small text-truncate mb-1">{{ r.filename }}</p>
|
||||
<p class="small mb-1">Wgrano: {{ r.uploaded_at.strftime('%Y-%m-%d %H:%M') }}</p>
|
||||
{% if r.filesize and r.filesize >= 1024 * 1024 %}
|
||||
<p class="small mb-1">Rozmiar: {{ (r.filesize / 1024 / 1024) | round(2) }} MB</p>
|
||||
{% elif r.filesize %}
|
||||
<p class="small mb-1">Rozmiar: {{ (r.filesize / 1024) | round(1) }} kB</p>
|
||||
{% else %}
|
||||
<p class="small mb-1 text-muted">Brak danych o rozmiarze</p>
|
||||
{% endif %}
|
||||
<div class="row">
|
||||
{% for r in receipts %}
|
||||
<div class="col-6 col-md-4 col-lg-3">
|
||||
<div class="card bg-dark text-white h-100">
|
||||
<a href="{{ url_for('uploaded_file', filename=r.filename) }}" class="glightbox" data-gallery="receipts"
|
||||
data-title="{{ r.filename }}">
|
||||
<img src="{{ url_for('uploaded_file', filename=r.filename) }}" class="card-img-top"
|
||||
style="object-fit: cover; height: 200px;">
|
||||
</a>
|
||||
<div class="card-body text-center">
|
||||
<p class="small text-truncate mb-1">{{ r.filename }}</p>
|
||||
<p class="small mb-1">Wgrano: {{ r.uploaded_at.strftime('%Y-%m-%d %H:%M') }}</p>
|
||||
{% if r.filesize and r.filesize >= 1024 * 1024 %}
|
||||
<p class="small mb-1">Rozmiar: {{ (r.filesize / 1024 / 1024) | round(2) }} MB</p>
|
||||
{% elif r.filesize %}
|
||||
<p class="small mb-1">Rozmiar: {{ (r.filesize / 1024) | round(1) }} kB</p>
|
||||
{% else %}
|
||||
<p class="small mb-1 text-muted">Brak danych o rozmiarze</p>
|
||||
{% endif %}
|
||||
|
||||
<a href="{{ url_for('rotate_receipt_user', receipt_id=r.id) }}"
|
||||
class="btn btn-sm btn-outline-warning w-100 mb-2">🔄 Obróć o 90°</a>
|
||||
<a href="{{ url_for('rotate_receipt_user', receipt_id=r.id) }}"
|
||||
class="btn btn-sm btn-outline-warning w-100 mb-2">🔄 Obróć o 90°</a>
|
||||
|
||||
<a href="#" class="btn btn-sm btn-outline-secondary w-100 mb-2" data-bs-toggle="modal"
|
||||
data-bs-target="#userCropModal" data-img-src="{{ url_for('uploaded_file', filename=r.filename) }}"
|
||||
data-receipt-id="{{ r.id }}" data-crop-endpoint="{{ url_for('crop_receipt_user') }}">
|
||||
✂️ Przytnij
|
||||
</a>
|
||||
<a href="{{ url_for('delete_receipt_user', receipt_id=r.id) }}" class="btn btn-sm btn-outline-danger w-100"
|
||||
onclick="return confirm('Na pewno usunąć ten paragon?')">🗑️ Usuń</a>
|
||||
</div>
|
||||
</div>
|
||||
<a href="#" class="btn btn-sm btn-outline-secondary w-100 mb-2" data-bs-toggle="modal"
|
||||
data-bs-target="#userCropModal" data-img-src="{{ url_for('uploaded_file', filename=r.filename) }}"
|
||||
data-receipt-id="{{ r.id }}" data-crop-endpoint="{{ url_for('crop_receipt_user') }}">
|
||||
✂️ Przytnij
|
||||
</a>
|
||||
<a href="{{ url_for('delete_receipt_user', receipt_id=r.id) }}" class="btn btn-sm btn-outline-danger w-100"
|
||||
onclick="return confirm('Na pewno usunąć ten paragon?')">🗑️ Usuń</a>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<hr class="my-3">
|
||||
<!-- Trigger przycisk -->
|
||||
<div class="btn-group mt-4" role="group">
|
||||
<button type="button" class="btn btn-outline-danger" data-bs-toggle="modal" data-bs-target="#deleteModal">
|
||||
🗑️ Usuń tę listę
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<hr class="my-3">
|
||||
<!-- Trigger przycisk -->
|
||||
<div class="btn-group mt-4" role="group">
|
||||
<button type="button" class="btn btn-outline-danger" data-bs-toggle="modal" data-bs-target="#deleteModal">
|
||||
🗑️ Usuń tę listę
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- MODAL -->
|
||||
<div class="modal fade" id="deleteModal" tabindex="-1" aria-labelledby="deleteModalLabel" aria-hidden="true">
|
||||
@@ -212,5 +200,4 @@
|
||||
<script src="{{ url_for('static_bp.serve_js', filename='user_receipt_crop.js') }}"></script>
|
||||
<script src="{{ url_for('static_bp.serve_js', filename='receipt_crop_logic.js') }}"></script>
|
||||
<script src="{{ url_for('static_bp.serve_js', filename='select.js') }}"></script>
|
||||
<script src="{{ url_for('static_bp.serve_js', filename='move_to_month.js') }}"></script>
|
||||
{% endblock %}
|
@@ -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