15 Commits

Author SHA1 Message Date
Mateusz Gruszczyński
68f235d605 fix w sugestiach i js 2025-08-15 23:29:13 +02:00
Mateusz Gruszczyński
ea46dd43e1 fix w sugestiach 2025-08-15 23:03:26 +02:00
Mateusz Gruszczyński
4b99b109bd fix w sugestiach 2025-08-15 22:29:40 +02:00
Mateusz Gruszczyński
028ae3c26e fix w sugestiach 2025-08-15 22:25:22 +02:00
Mateusz Gruszczyński
71b14411e5 usuniecie zbednego kodu i poprawki 2025-08-15 15:54:40 +02:00
Mateusz Gruszczyński
f1744fae99 usuniecie zbednego kodu i poprawki 2025-08-15 15:53:40 +02:00
Mateusz Gruszczyński
79c6f7d0b1 usuniecie zbednego kodu i poprawki 2025-08-15 15:52:49 +02:00
Mateusz Gruszczyński
80651bc3c7 usuniecie zbednego kodu i poprawki 2025-08-15 15:51:53 +02:00
Mateusz Gruszczyński
4602fb7749 usuniecie zbednego kodu i poprawki 2025-08-15 15:50:49 +02:00
Mateusz Gruszczyński
40381774b4 usuniecie zbednego kodu i poprawki 2025-08-15 15:48:43 +02:00
Mateusz Gruszczyński
cc988d5934 usuniecie zbednego kodu i poprawki 2025-08-15 15:47:32 +02:00
Mateusz Gruszczyński
883562c532 usuniecie zbednego kodu 2025-08-15 15:41:02 +02:00
Mateusz Gruszczyński
5e01a735d3 paginacja i poprawki uxowe 2025-08-15 13:25:41 +02:00
Mateusz Gruszczyński
4988ad9a5f cofnięcie zmian z przesuwaniem listy 2025-08-15 13:23:34 +02:00
Mateusz Gruszczyński
d321521ef1 cofnięcie zmian z przesuwaniem listy 2025-08-15 13:22:47 +02:00
13 changed files with 259 additions and 395 deletions

108
app.py
View File

@@ -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")

View File

@@ -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);
});
});

View File

@@ -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();

View File

@@ -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);
});
});

View File

@@ -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 %}">

View File

@@ -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 %}

View File

@@ -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 %}

View File

@@ -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"

View File

@@ -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>

View File

@@ -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 %}

View File

@@ -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 %}

View File

@@ -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>

View File

@@ -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>