zmiany w funkcja oraz UX
This commit is contained in:
139
app.py
139
app.py
@@ -284,6 +284,7 @@ class Receipt(db.Model):
|
||||
filesize = db.Column(db.Integer, nullable=True)
|
||||
file_hash = db.Column(db.String(64), nullable=True, unique=True)
|
||||
uploaded_by = db.Column(db.Integer, db.ForeignKey("user.id"))
|
||||
version_token = db.Column(db.String(32), nullable=True)
|
||||
|
||||
shopping_list = db.relationship("ShoppingList", back_populates="receipts")
|
||||
uploaded_by_user = db.relationship("User", backref="uploaded_receipts")
|
||||
@@ -408,6 +409,8 @@ app.register_blueprint(static_bp)
|
||||
def allowed_file(filename):
|
||||
return "." in filename and filename.rsplit(".", 1)[1].lower() in ALLOWED_EXTENSIONS
|
||||
|
||||
def generate_version_token():
|
||||
return secrets.token_hex(8)
|
||||
|
||||
def get_list_details(list_id):
|
||||
shopping_list = ShoppingList.query.options(
|
||||
@@ -419,9 +422,9 @@ def get_list_details(list_id):
|
||||
items = sorted(shopping_list.items, key=lambda i: i.position or 0)
|
||||
expenses = shopping_list.expenses
|
||||
total_expense = sum(e.amount for e in expenses) if expenses else 0
|
||||
receipt_files = [r.filename for r in shopping_list.receipts]
|
||||
receipts = shopping_list.receipts
|
||||
|
||||
return shopping_list, items, receipt_files, expenses, total_expense
|
||||
return shopping_list, items, receipts, expenses, total_expense
|
||||
|
||||
|
||||
def get_total_expense_for_list(list_id, start_date=None, end_date=None):
|
||||
@@ -505,7 +508,8 @@ def save_resized_image(file, path):
|
||||
image.info.clear()
|
||||
|
||||
new_path = path.rsplit(".", 1)[0] + ".webp"
|
||||
image.save(new_path, **WEBP_SAVE_PARAMS)
|
||||
#image.save(new_path, **WEBP_SAVE_PARAMS)
|
||||
image.save(new_path, format="WEBP", method=6, quality=100)
|
||||
|
||||
except Exception as e:
|
||||
raise ValueError(f"Błąd podczas przetwarzania obrazu: {e}")
|
||||
@@ -569,23 +573,27 @@ def receipt_error(message):
|
||||
|
||||
def rotate_receipt_by_id(receipt_id):
|
||||
receipt = Receipt.query.get_or_404(receipt_id)
|
||||
old_path = os.path.join(app.config["UPLOAD_FOLDER"], receipt.filename)
|
||||
path = os.path.join(app.config["UPLOAD_FOLDER"], receipt.filename)
|
||||
|
||||
if not os.path.exists(old_path):
|
||||
if not os.path.exists(path):
|
||||
raise FileNotFoundError("Plik nie istnieje")
|
||||
|
||||
image = Image.open(old_path)
|
||||
rotated = image.rotate(-90, expand=True)
|
||||
try:
|
||||
image = Image.open(path)
|
||||
rotated = image.rotate(-90, expand=True)
|
||||
|
||||
new_filename = generate_new_receipt_filename(receipt.list_id)
|
||||
new_path = os.path.join(app.config["UPLOAD_FOLDER"], new_filename)
|
||||
rotated.save(new_path, **WEBP_SAVE_PARAMS)
|
||||
rotated = rotated.convert("RGB")
|
||||
rotated.info.clear()
|
||||
|
||||
os.remove(old_path)
|
||||
receipt.filename = new_filename
|
||||
db.session.commit()
|
||||
rotated.save(path, format="WEBP", method=6, quality=100)
|
||||
receipt.version_token = generate_version_token()
|
||||
recalculate_filesizes(receipt.id)
|
||||
db.session.commit()
|
||||
|
||||
return receipt
|
||||
return receipt
|
||||
except Exception as e:
|
||||
app.logger.exception("Błąd podczas rotacji pliku")
|
||||
raise RuntimeError(f"Błąd podczas rotacji pliku: {e}")
|
||||
|
||||
|
||||
def delete_receipt_by_id(receipt_id):
|
||||
@@ -610,23 +618,18 @@ def handle_crop_receipt(receipt_id, file):
|
||||
if not receipt_id or not file:
|
||||
return {"success": False, "error": "Brak danych"}
|
||||
|
||||
receipt = Receipt.query.get_or_404(receipt_id)
|
||||
old_path = os.path.join(app.config["UPLOAD_FOLDER"], receipt.filename)
|
||||
|
||||
try:
|
||||
new_filename = generate_new_receipt_filename(receipt.list_id)
|
||||
new_path = os.path.join(app.config["UPLOAD_FOLDER"], new_filename)
|
||||
receipt = Receipt.query.get_or_404(receipt_id)
|
||||
path = os.path.join(app.config["UPLOAD_FOLDER"], receipt.filename)
|
||||
|
||||
save_resized_image(file, new_path)
|
||||
|
||||
if os.path.exists(old_path):
|
||||
os.remove(old_path)
|
||||
|
||||
receipt.filename = os.path.basename(new_path)
|
||||
db.session.commit()
|
||||
save_resized_image(file, path)
|
||||
receipt.version_token = generate_version_token()
|
||||
recalculate_filesizes(receipt.id)
|
||||
db.session.commit()
|
||||
|
||||
return {"success": True}
|
||||
except Exception as e:
|
||||
app.logger.exception("Błąd podczas przycinania paragonu")
|
||||
return {"success": False, "error": str(e)}
|
||||
|
||||
|
||||
@@ -1760,7 +1763,7 @@ def view_list(list_id):
|
||||
flash("W celu modyfikacji listy, przejdź do panelu administracyjnego.", "info")
|
||||
return redirect(url_for("shared_list", token=shopping_list.share_token))
|
||||
|
||||
shopping_list, items, receipt_files, expenses, total_expense = get_list_details(
|
||||
shopping_list, items, receipts, expenses, total_expense = get_list_details(
|
||||
list_id
|
||||
)
|
||||
total_count = len(items)
|
||||
@@ -1785,7 +1788,7 @@ def view_list(list_id):
|
||||
"list.html",
|
||||
list=shopping_list,
|
||||
items=items,
|
||||
receipt_files=receipt_files,
|
||||
receipts=receipts,
|
||||
total_count=total_count,
|
||||
purchased_count=purchased_count,
|
||||
percent=percent,
|
||||
@@ -1960,7 +1963,7 @@ def shared_list(token=None, list_id=None):
|
||||
list_id = shopping_list.id
|
||||
|
||||
total_expense = get_total_expense_for_list(list_id)
|
||||
shopping_list, items, receipt_files, expenses, total_expense = get_list_details(
|
||||
shopping_list, items, receipts, expenses, total_expense = get_list_details(
|
||||
list_id
|
||||
)
|
||||
|
||||
@@ -1981,7 +1984,7 @@ def shared_list(token=None, list_id=None):
|
||||
"list_share.html",
|
||||
list=shopping_list,
|
||||
items=items,
|
||||
receipt_files=receipt_files,
|
||||
receipts=receipts,
|
||||
expenses=expenses,
|
||||
total_expense=total_expense,
|
||||
is_share=True,
|
||||
@@ -2130,66 +2133,60 @@ def all_products():
|
||||
@app.route("/upload_receipt/<int:list_id>", methods=["POST"])
|
||||
@login_required
|
||||
def upload_receipt(list_id):
|
||||
|
||||
l = db.session.get(ShoppingList, list_id)
|
||||
|
||||
if "receipt" not in request.files:
|
||||
return receipt_error("Brak pliku")
|
||||
|
||||
file = request.files["receipt"]
|
||||
if file.filename == "":
|
||||
file = request.files.get("receipt")
|
||||
if not file or file.filename == "":
|
||||
return receipt_error("Nie wybrano pliku")
|
||||
|
||||
if file and allowed_file(file.filename):
|
||||
file_bytes = file.read()
|
||||
file.seek(0)
|
||||
file_hash = hashlib.sha256(file_bytes).hexdigest()
|
||||
if not allowed_file(file.filename):
|
||||
return receipt_error("Niedozwolony format pliku")
|
||||
|
||||
existing = Receipt.query.filter_by(file_hash=file_hash).first()
|
||||
if existing:
|
||||
return receipt_error("Taki plik już istnieje")
|
||||
file_bytes = file.read()
|
||||
file.seek(0)
|
||||
file_hash = hashlib.sha256(file_bytes).hexdigest()
|
||||
|
||||
now = datetime.now(timezone.utc)
|
||||
timestamp = now.strftime("%Y%m%d_%H%M")
|
||||
random_part = secrets.token_hex(3)
|
||||
webp_filename = f"list_{list_id}_{timestamp}_{random_part}.webp"
|
||||
file_path = os.path.join(app.config["UPLOAD_FOLDER"], webp_filename)
|
||||
existing = Receipt.query.filter_by(file_hash=file_hash).first()
|
||||
if existing:
|
||||
return receipt_error("Taki plik już istnieje")
|
||||
|
||||
try:
|
||||
if file.filename.lower().endswith(".pdf"):
|
||||
file.seek(0)
|
||||
save_pdf_as_webp(file, file_path)
|
||||
else:
|
||||
save_resized_image(file, file_path)
|
||||
except ValueError as e:
|
||||
return receipt_error(str(e))
|
||||
now = datetime.now(timezone.utc)
|
||||
timestamp = now.strftime("%Y%m%d_%H%M")
|
||||
random_part = secrets.token_hex(3)
|
||||
webp_filename = f"list_{list_id}_{timestamp}_{random_part}.webp"
|
||||
file_path = os.path.join(app.config["UPLOAD_FOLDER"], webp_filename)
|
||||
|
||||
filesize = os.path.getsize(file_path)
|
||||
uploaded_at = datetime.now(timezone.utc)
|
||||
try:
|
||||
if file.filename.lower().endswith(".pdf"):
|
||||
file.seek(0)
|
||||
save_pdf_as_webp(file, file_path)
|
||||
else:
|
||||
save_resized_image(file, file_path)
|
||||
except ValueError as e:
|
||||
return receipt_error(str(e))
|
||||
|
||||
try:
|
||||
new_receipt = Receipt(
|
||||
list_id=list_id,
|
||||
filename=webp_filename,
|
||||
filesize=filesize,
|
||||
uploaded_at=uploaded_at,
|
||||
filesize=os.path.getsize(file_path),
|
||||
uploaded_at=now,
|
||||
file_hash=file_hash,
|
||||
uploaded_by=current_user.id,
|
||||
version_token=generate_version_token(),
|
||||
)
|
||||
db.session.add(new_receipt)
|
||||
db.session.commit()
|
||||
except Exception as e:
|
||||
return receipt_error(f"Błąd zapisu do bazy: {str(e)}")
|
||||
|
||||
if (
|
||||
request.is_json
|
||||
or request.headers.get("X-Requested-With") == "XMLHttpRequest"
|
||||
):
|
||||
url = url_for("uploaded_file", filename=webp_filename)
|
||||
socketio.emit("receipt_added", {"url": url}, to=str(list_id))
|
||||
return jsonify({"success": True, "url": url})
|
||||
if request.is_json or request.headers.get("X-Requested-With") == "XMLHttpRequest":
|
||||
url = url_for("uploaded_file", filename=webp_filename) + f"?v={new_receipt.version_token or '0'}"
|
||||
socketio.emit("receipt_added", {"url": url}, to=str(list_id))
|
||||
return jsonify({"success": True, "url": url})
|
||||
|
||||
flash("Wgrano paragon", "success")
|
||||
return redirect(request.referrer or url_for("main_page"))
|
||||
|
||||
return receipt_error("Niedozwolony format pliku")
|
||||
flash("Wgrano paragon", "success")
|
||||
return redirect(request.referrer or url_for("main_page"))
|
||||
|
||||
|
||||
@app.route("/uploads/<filename>")
|
||||
|
@@ -7,7 +7,7 @@
|
||||
<a href="/" class="btn btn-outline-secondary">← Powrót do strony głównej</a>
|
||||
</div>
|
||||
|
||||
<div class="card bg-dark text-white mb-4">
|
||||
<div class="card bg-secondary bg-opacity-10 text-white mb-4">
|
||||
<div class="card-body p-2">
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
<a href="{{ url_for('list_users') }}" class="btn btn-outline-light btn-sm">👥 Użytkownicy</a>
|
||||
|
@@ -7,7 +7,7 @@
|
||||
<a href="{{ url_for('admin_panel') }}" class="btn btn-outline-secondary">← Powrót do panelu</a>
|
||||
</div>
|
||||
|
||||
<div class="card bg-dark text-white mb-5">
|
||||
<div class="card bg-secondary bg-opacity-10 text-white mb-5">
|
||||
<div class="card-body">
|
||||
<h4 class="card-title">📄 Podstawowe informacje</h4>
|
||||
<form method="post" class="mt-3">
|
||||
@@ -123,7 +123,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card bg-dark text-white mb-5">
|
||||
<div class="card bg-secondary bg-opacity-10 text-white mb-5">
|
||||
<div class="card-body">
|
||||
<h4 class="card-title">📝 Produkty</h4>
|
||||
|
||||
@@ -226,7 +226,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card bg-dark text-white mb-5">
|
||||
<div class="card bg-secondary bg-opacity-10 text-white mb-5">
|
||||
<div class="card-body">
|
||||
<h4 class="card-title">🧾 Paragony</h4>
|
||||
|
||||
@@ -239,25 +239,27 @@
|
||||
<div class="row g-3">
|
||||
{% 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;">
|
||||
<div class="card bg-dark text-white h-100 shadow-sm border border-secondary">
|
||||
<a href="{{ url_for('uploaded_file', filename=r.filename) }}?v={{ r.version_token or '0' }}" class="glightbox"
|
||||
data-gallery="receipts" data-title="{{ r.filename }}">
|
||||
<img src="{{ url_for('uploaded_file', filename=r.filename) }}?v={{ r.version_token or '0' }}"
|
||||
class="card-img-top" style="object-fit: cover; height: 200px;" title="{{ r.filename }}">
|
||||
</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>
|
||||
<p class="small mb-1">
|
||||
Uploader: {{ r.uploaded_by_user.username if r.uploaded_by_user else "?" }}
|
||||
</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="card-body text-center p-2 small">
|
||||
<div class="text-truncate fw-semibold" title="{{ r.filename }}">📄 {{ r.filename }}</div>
|
||||
<div>📅 {{ r.uploaded_at.strftime('%Y-%m-%d %H:%M') }}</div>
|
||||
<div>👤 {{ r.uploaded_by_user.username if r.uploaded_by_user else "?" }}</div>
|
||||
<div>
|
||||
💾
|
||||
{% if r.filesize and r.filesize >= 1024 * 1024 %}
|
||||
{{ (r.filesize / 1024 / 1024) | round(2) }} MB
|
||||
{% elif r.filesize %}
|
||||
{{ (r.filesize / 1024) | round(1) }} kB
|
||||
{% else %}
|
||||
Brak danych
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -7,7 +7,7 @@
|
||||
<a href="{{ url_for('admin_panel') }}" class="btn btn-outline-secondary">← Powrót do panelu</a>
|
||||
</div>
|
||||
|
||||
<div class="card bg-dark text-white mb-4">
|
||||
<div class="card bg-secondary bg-opacity-10 text-white mb-4">
|
||||
<div class="card-body">
|
||||
|
||||
<!-- Formularz dodawania sugestii -->
|
||||
|
@@ -3,92 +3,102 @@
|
||||
{% block content %}
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center flex-wrap mb-4">
|
||||
<h2 class="mb-2">🗂 Masowa edycja kategorii list</h2>
|
||||
<h2 class="mb-2">🗂 Masowa edycja kategorii</h2>
|
||||
<div>
|
||||
<a href="{{ url_for('admin_panel') }}" class="btn btn-outline-secondary">← Powrót do panelu</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="alert alert-warning border-warning text-dark" role="alert">
|
||||
<strong>Uwaga!</strong> Przypisanie więcej niż jednej kategorii do listy może zaburzyć
|
||||
poprawne zliczanie wydatków, ponieważ wydatki tej listy będą jednocześnie
|
||||
klasyfikowane do kilku kategorii.
|
||||
|
||||
<div class="card bg-secondary bg-opacity-10 text-white mb-5">
|
||||
<div class="card-body">
|
||||
<div class="alert alert-warning border-warning text-dark" role="alert">
|
||||
<strong>Uwaga!</strong> Przypisanie więcej niż jednej kategorii do listy może zaburzyć
|
||||
poprawne zliczanie wydatków, ponieważ wydatki tej listy będą jednocześnie
|
||||
klasyfikowane do kilku kategorii.
|
||||
</div>
|
||||
|
||||
<form method="post">
|
||||
<div class="card bg-dark text-white mb-5">
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-dark table-striped align-middle sortable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">ID</th>
|
||||
<th scope="col">Nazwa listy</th>
|
||||
<th scope="col">Właściciel</th>
|
||||
<th scope="col">Data utworzenia</th>
|
||||
<th scope="col">Status</th>
|
||||
<th scope="col">Podgląd produktów</th>
|
||||
<th scope="col">Kategorie</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for l in lists %}
|
||||
<tr>
|
||||
<td>{{ l.id }}</td>
|
||||
<td class="fw-bold align-middle">
|
||||
<a href="{{ url_for('view_list', list_id=l.id) }}" class="text-white">{{ l.title
|
||||
}}</a>
|
||||
</td>
|
||||
<td>
|
||||
{% if l.owner %}
|
||||
👤 {{ l.owner.username }} ({{ l.owner.id }})
|
||||
{% else %}
|
||||
-
|
||||
{% endif %}
|
||||
</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 rounded-pill bg-secondary">Archiwalna</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-light preview-btn"
|
||||
data-list-id="{{ l.id }}">
|
||||
🔍 Zobacz
|
||||
</button>
|
||||
</td>
|
||||
<td style="min-width: 220px;">
|
||||
<select name="categories_{{ l.id }}" multiple
|
||||
class="form-select tom-dark bg-dark text-white border-secondary rounded">
|
||||
{% for cat in categories %}
|
||||
<option value="{{ cat.id }}" {% if cat in l.categories %}selected{% endif
|
||||
%}>
|
||||
{{ cat.name }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% if lists|length == 0 %}
|
||||
<tr>
|
||||
<td colspan="12" class="text-center py-4">
|
||||
Brak list zakupowych do wyświetlenia
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<button type="submit" class="btn btn-sm btn-outline-light">💾 Zapisz zmiany</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form method="post">
|
||||
<div class="card bg-dark text-white mb-5">
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-dark table-striped align-middle sortable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">ID</th>
|
||||
<th scope="col">Nazwa listy</th>
|
||||
<th scope="col">Właściciel</th>
|
||||
<th scope="col">Data utworzenia</th>
|
||||
<th scope="col">Status</th>
|
||||
<th scope="col">Podgląd produktów</th>
|
||||
<th scope="col">Kategorie</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for l in lists %}
|
||||
<tr>
|
||||
<td>{{ l.id }}</td>
|
||||
<td class="fw-bold align-middle">
|
||||
<a href="{{ url_for('view_list', list_id=l.id) }}" class="text-white">{{ l.title }}</a>
|
||||
</td>
|
||||
<td>
|
||||
{% if l.owner %}
|
||||
👤 {{ l.owner.username }} ({{ l.owner.id }})
|
||||
{% else %}
|
||||
-
|
||||
{% endif %}
|
||||
</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 rounded-pill bg-secondary">Archiwalna</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-light preview-btn"
|
||||
data-list-id="{{ l.id }}">
|
||||
🔍 Zobacz
|
||||
</button>
|
||||
</td>
|
||||
<td style="min-width: 220px;">
|
||||
<select name="categories_{{ l.id }}" multiple
|
||||
class="form-select tom-dark bg-dark text-white border-secondary rounded">
|
||||
{% for cat in categories %}
|
||||
<option value="{{ cat.id }}" {% if cat in l.categories %}selected{% endif %}>
|
||||
{{ cat.name }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% if lists|length == 0 %}
|
||||
<tr>
|
||||
<td colspan="12" class="text-center py-4">
|
||||
Brak list zakupowych do wyświetlenia
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<button type="submit" class="btn btn-sm btn-outline-light">💾 Zapisz zmiany</button>
|
||||
</div>
|
||||
</form>
|
||||
<hr>
|
||||
<div class="d-flex justify-content-between align-items-center mt-4">
|
||||
<form method="get" class="d-flex align-items-center">
|
||||
@@ -109,7 +119,8 @@
|
||||
</li>
|
||||
{% for p in range(1, total_pages + 1) %}
|
||||
<li class="page-item {% if p == page %}active{% endif %}">
|
||||
<a class="page-link" href="?{{ query_string }}{% if query_string %}&{% endif %}page={{ p }}">{{ p }}</a>
|
||||
<a class="page-link" href="?{{ query_string }}{% if query_string %}&{% endif %}page={{ p }}">{{
|
||||
p }}</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
<li class="page-item {% if page >= total_pages %}disabled{% endif %}">
|
||||
|
@@ -31,7 +31,6 @@
|
||||
</strong>
|
||||
</p>
|
||||
|
||||
|
||||
<div>
|
||||
<a href="{{ url_for('recalculate_filesizes_all') }}" class="btn btn-outline-light me-2">
|
||||
Przelicz rozmiary plików
|
||||
@@ -40,55 +39,74 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card bg-dark text-white mb-5">
|
||||
<div class="card bg-secondary bg-opacity-10 text-white mb-5">
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
{% 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;">
|
||||
<div class="card bg-dark text-white h-100 shadow-sm border border-secondary">
|
||||
<a href="{{ url_for('uploaded_file', filename=r.filename) }}?v={{ r.version_token or '0' }}" class="glightbox"
|
||||
data-gallery="receipts" data-title="{{ r.filename }}">
|
||||
<img src="{{ url_for('uploaded_file', filename=r.filename) }}?v={{ r.version_token or '0' }}"
|
||||
class="card-img-top" style="object-fit: cover; height: 200px;"
|
||||
title="Token: {{ r.version_token or '0' }}">
|
||||
</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>
|
||||
<p class="small mb-1">
|
||||
Uploader: {{ r.uploaded_by_user.username if r.uploaded_by_user else "?" }}
|
||||
</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', receipt_id=r.id) }}"
|
||||
class="btn btn-sm btn-outline-light w-100 mb-2">🔄 Obróć o 90°</a>
|
||||
<a href="#" class="btn btn-sm btn-outline-light w-100 mb-2" data-bs-toggle="modal"
|
||||
data-bs-target="#adminCropModal" data-img-src="{{ url_for('uploaded_file', filename=r.filename) }}"
|
||||
data-receipt-id="{{ r.id }}" data-crop-endpoint="{{ url_for('crop_receipt_admin') }}">
|
||||
✂️ Przytnij
|
||||
</a>
|
||||
<a href="{{ url_for('rename_receipt', receipt_id=r.id) }}"
|
||||
class="btn btn-sm btn-outline-light w-100 mb-2">✏️
|
||||
Zmień nazwę</a>
|
||||
{% if not r.file_hash %}
|
||||
<a href="{{ url_for('generate_receipt_hash', receipt_id=r.id) }}"
|
||||
class="btn btn-sm btn-outline-light w-100 mb-2">🔐 Generuj hash</a>
|
||||
{% endif %}
|
||||
<a href="{{ url_for('delete_receipt', receipt_id=r.id) }}" class="btn btn-sm btn-outline-light w-100 mb-2"
|
||||
onclick="return confirm('Na pewno usunąć plik {{ r.filename }}?');">🗑️
|
||||
Usuń</a>
|
||||
<a href="{{ url_for('edit_list', list_id=r.list_id) }}" class="btn btn-sm btn-outline-light w-100 mb-2">✏️
|
||||
Edytuj listę #{{ r.list_id }}</a>
|
||||
<div class="card-body text-center p-2 small">
|
||||
<div class="text-truncate fw-semibold" title="{{ r.filename }}">📄 {{ r.filename }}</div>
|
||||
<div>📅 {{ r.uploaded_at.strftime('%Y-%m-%d %H:%M') }}</div>
|
||||
<div>👤 {{ r.uploaded_by_user.username if r.uploaded_by_user else "?" }}</div>
|
||||
<div>
|
||||
💾
|
||||
{% if r.filesize and r.filesize >= 1024 * 1024 %}
|
||||
{{ (r.filesize / 1024 / 1024) | round(2) }} MB
|
||||
{% elif r.filesize %}
|
||||
{{ (r.filesize / 1024) | round(1) }} kB
|
||||
{% else %}
|
||||
Brak danych
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="dropdown mt-2">
|
||||
<button class="btn btn-sm btn-outline-light dropdown-toggle w-100" type="button"
|
||||
data-bs-toggle="dropdown">
|
||||
⋮ Akcje
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-dark w-100 text-start">
|
||||
<li>
|
||||
<a class="dropdown-item" href="{{ url_for('rotate_receipt', receipt_id=r.id) }}">🔄 Obróć o 90°</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="dropdown-item" href="#" data-bs-toggle="modal" data-bs-target="#adminCropModal"
|
||||
data-img-src="{{ url_for('uploaded_file', filename=r.filename) }}" data-receipt-id="{{ r.id }}"
|
||||
data-crop-endpoint="{{ url_for('crop_receipt_admin') }}">✂️ Przytnij</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="dropdown-item" href="{{ url_for('rename_receipt', receipt_id=r.id) }}">✏️ Zmień nazwę</a>
|
||||
</li>
|
||||
{% if not r.file_hash %}
|
||||
<li>
|
||||
<a class="dropdown-item" href="{{ url_for('generate_receipt_hash', receipt_id=r.id) }}">🔐 Generuj
|
||||
hash</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
<li>
|
||||
<a class="dropdown-item text-danger" href="{{ url_for('delete_receipt', receipt_id=r.id) }}"
|
||||
onclick="return confirm('Na pewno usunąć plik {{ r.filename }}?');">🗑️ Usuń</a>
|
||||
</li>
|
||||
<li>
|
||||
<hr class="dropdown-divider">
|
||||
</li>
|
||||
<li>
|
||||
<a class="dropdown-item" href="{{ url_for('edit_list', list_id=r.list_id) }}">📋 Edytuj listę #{{
|
||||
r.list_id }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
</div>
|
||||
|
||||
{% if not receipts %}
|
||||
@@ -98,6 +116,7 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mt-4">
|
||||
|
@@ -8,7 +8,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Formularz dodawania nowego użytkownika -->
|
||||
<div class="card bg-dark text-white mb-4">
|
||||
<div class="card bg-secondary bg-opacity-10 text-white mb-4">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title mb-3">➕ Dodaj nowego użytkownika</h5>
|
||||
<form method="post" action="{{ url_for('add_user') }}">
|
||||
|
@@ -6,7 +6,7 @@
|
||||
<a href="{{ url_for('main_page') }}" class="btn btn-outline-secondary">← Powrót</a>
|
||||
</div>
|
||||
|
||||
<div class="card bg-dark text-white mb-5">
|
||||
<div class="card bg-secondary bg-opacity-10 text-white mb-5">
|
||||
<div class="card-body">
|
||||
<form method="post">
|
||||
|
||||
@@ -103,33 +103,50 @@
|
||||
<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;">
|
||||
<div class="card bg-dark text-white h-100 shadow-sm border border-secondary">
|
||||
<a href="{{ url_for('uploaded_file', filename=r.filename) }}?v={{ r.version_token or '0' }}" class="glightbox"
|
||||
data-gallery="receipts" data-title="{{ r.filename }}">
|
||||
<img src="{{ url_for('uploaded_file', filename=r.filename) }}?v={{ r.version_token or '0' }}"
|
||||
class="card-img-top" style="object-fit: cover; height: 200px;" title="{{ r.filename }}">
|
||||
</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-light w-100 mb-2">🔄 Obróć o 90°</a>
|
||||
<div class="card-body text-center p-2 small">
|
||||
<div class="text-truncate fw-semibold" title="{{ r.filename }}">📄 {{ r.filename }}</div>
|
||||
<div>📅 {{ r.uploaded_at.strftime('%Y-%m-%d %H:%M') }}</div>
|
||||
<div>
|
||||
💾
|
||||
{% if r.filesize and r.filesize >= 1024 * 1024 %}
|
||||
{{ (r.filesize / 1024 / 1024) | round(2) }} MB
|
||||
{% elif r.filesize %}
|
||||
{{ (r.filesize / 1024) | round(1) }} kB
|
||||
{% else %}
|
||||
Brak danych
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<a href="#" class="btn btn-sm btn-outline-light 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-light w-100"
|
||||
onclick="return confirm('Na pewno usunąć ten paragon?')">🗑️ Usuń</a>
|
||||
<div class="dropdown mt-2">
|
||||
<button class="btn btn-sm btn-outline-light dropdown-toggle w-100" type="button" data-bs-toggle="dropdown">
|
||||
⋮ Akcje
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-dark w-100 text-start">
|
||||
<li>
|
||||
<a class="dropdown-item" href="{{ url_for('rotate_receipt_user', receipt_id=r.id) }}">🔄 Obróć o 90°</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="dropdown-item" href="#" 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>
|
||||
</li>
|
||||
<li>
|
||||
<a class="dropdown-item text-danger" href="{{ url_for('delete_receipt_user', receipt_id=r.id) }}"
|
||||
onclick="return confirm('Na pewno usunąć ten paragon?')">
|
||||
🗑️ Usuń
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -7,29 +7,33 @@
|
||||
<a href="{{ url_for('main_page') }}" class="btn btn-outline-secondary">← Powrót</a>
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-center mb-3">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" id="showAllLists" {% if show_all %}checked{% endif %}>
|
||||
<label class="form-check-label ms-2 text-white" for="showAllLists">
|
||||
Pokaż wszystkie publiczne listy innych
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card bg-secondary bg-opacity-10 text-white mb-5">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-center mb-3">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" id="showAllLists" {% if show_all %}checked{% endif %}>
|
||||
<label class="form-check-label ms-2 text-white" for="showAllLists">
|
||||
Pokaż wszystkie publiczne listy innych
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Przyciski kategorii -->
|
||||
<div class="d-flex flex-wrap gap-2 mb-3 justify-content-center">
|
||||
<button type="button"
|
||||
class="btn btn-sm category-filter {% if not selected_category %}btn-success{% else %}btn-outline-light{% endif %}"
|
||||
data-category-id="">
|
||||
🌐 Wszystkie
|
||||
</button>
|
||||
{% for cat in categories %}
|
||||
<button type="button"
|
||||
class="btn btn-sm category-filter {% if selected_category == cat.id %}btn-success{% else %}btn-outline-light{% endif %}"
|
||||
data-category-id="{{ cat.id }}">
|
||||
{{ cat.name }}
|
||||
</button>
|
||||
{% endfor %}
|
||||
<!-- Przyciski kategorii -->
|
||||
<div class="d-flex flex-wrap gap-2 mb-3 justify-content-center">
|
||||
<button type="button"
|
||||
class="btn btn-sm category-filter {% if not selected_category %}btn-success{% else %}btn-outline-light{% endif %}"
|
||||
data-category-id="">
|
||||
🌐 Wszystkie
|
||||
</button>
|
||||
{% for cat in categories %}
|
||||
<button type="button"
|
||||
class="btn btn-sm category-filter {% if selected_category == cat.id %}btn-success{% else %}btn-outline-light{% endif %}"
|
||||
data-category-id="{{ cat.id }}">
|
||||
{{ cat.name }}
|
||||
</button>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card bg-dark text-white mb-5">
|
||||
@@ -136,7 +140,8 @@
|
||||
|
||||
<div class="d-flex flex-wrap gap-2 mb-3 justify-content-center">
|
||||
<div class="btn-group btn-group-sm" role="group">
|
||||
<button class="btn btn-outline-light range-btn active" data-range="last30days">🗓️ Ostatnie 30 dni</button>
|
||||
<button class="btn btn-outline-light range-btn active" data-range="last30days">🗓️ Ostatnie 30
|
||||
dni</button>
|
||||
<button class="btn btn-outline-light range-btn" data-range="currentmonth">📅 Bieżący miesiąc</button>
|
||||
<button class="btn btn-outline-light range-btn" data-range="monthly">📆 Miesięczne</button>
|
||||
<button class="btn btn-outline-light range-btn" data-range="quarterly">📊 Kwartalne</button>
|
||||
|
@@ -31,7 +31,7 @@
|
||||
list.is_public %}disabled{% endif %}>
|
||||
✅ Otwórz tryb zakupowy / odznaczania produktów
|
||||
</a>
|
||||
<div id="share-card" class="card bg-dark text-white mb-4">
|
||||
<div id="share-card" class="card bg-secondary bg-opacity-10 text-white mb-4">
|
||||
<div class="card-body">
|
||||
<div class="mb-2">
|
||||
<strong id="share-header">
|
||||
@@ -203,11 +203,12 @@
|
||||
<h5 class="mt-4">📸 Paragony dodane do tej listy</h5>
|
||||
|
||||
<div class="row g-3 mt-2" id="receiptGallery">
|
||||
{% if receipt_files %}
|
||||
{% for file in receipt_files %}
|
||||
{% if receipts %}
|
||||
{% for r in receipts %}
|
||||
<div class="col-6 col-md-4 col-lg-3 text-center">
|
||||
<a href="{{ url_for('uploaded_file', filename=file) }}" class="glightbox" data-gallery="receipt-gallery">
|
||||
<img src="{{ url_for('uploaded_file', filename=file) }}"
|
||||
<a href="{{ url_for('uploaded_file', filename=r.filename) }}?v={{ r.version_token or '0' }}" class="glightbox"
|
||||
data-gallery="receipt-gallery">
|
||||
<img src="{{ url_for('uploaded_file', filename=r.filename) }}?v={{ r.version_token or '0' }}"
|
||||
class="img-fluid rounded shadow-sm border border-secondary" style="max-height: 200px; object-fit: cover;">
|
||||
</a>
|
||||
</div>
|
||||
|
@@ -152,11 +152,12 @@
|
||||
|
||||
<h5 class="mt-4">📸 Paragony dodane do tej listy</h5>
|
||||
<div class="row g-3 mt-2" id="receiptGallery">
|
||||
{% if receipt_files %}
|
||||
{% for file in receipt_files %}
|
||||
{% if receipts %}
|
||||
{% for r in receipts %}
|
||||
<div class="col-6 col-md-4 col-lg-3 text-center">
|
||||
<a href="{{ url_for('uploaded_file', filename=file) }}" class="glightbox" data-gallery="receipt-gallery">
|
||||
<img src="{{ url_for('uploaded_file', filename=file) }}"
|
||||
<a href="{{ url_for('uploaded_file', filename=r.filename) }}?v={{ r.version_token or '0' }}" class="glightbox"
|
||||
data-gallery="receipt-gallery">
|
||||
<img src="{{ url_for('uploaded_file', filename=r.filename) }}?v={{ r.version_token or '0' }}"
|
||||
class="img-fluid rounded shadow-sm border border-secondary" style="max-height: 200px; object-fit: cover;">
|
||||
</a>
|
||||
</div>
|
||||
|
@@ -13,7 +13,7 @@
|
||||
<h2 class="mb-2">Stwórz nową listę</h2>
|
||||
</div>
|
||||
|
||||
<div class="card bg-dark text-white mb-4">
|
||||
<div class="card bg-secondary bg-opacity-10 text-white mb-4">
|
||||
<div class="card-body">
|
||||
<form action="/create" method="post">
|
||||
<div class="input-group mb-3">
|
||||
|
Reference in New Issue
Block a user