crop dla userów i przeniesienie listy na inny miesiac
This commit is contained in:
145
app.py
145
app.py
@@ -64,13 +64,17 @@ app = Flask(__name__)
|
||||
app.config.from_object(Config)
|
||||
|
||||
# Konfiguracja nagłówków bezpieczeństwa z .env
|
||||
csp_policy = {
|
||||
"default-src": "'self'",
|
||||
"script-src": "'self' 'unsafe-inline'",
|
||||
"style-src": "'self' 'unsafe-inline'",
|
||||
"img-src": "'self' data:",
|
||||
"connect-src": "'self'",
|
||||
} if app.config.get("ENABLE_CSP", True) else None
|
||||
csp_policy = (
|
||||
{
|
||||
"default-src": "'self'",
|
||||
"script-src": "'self' 'unsafe-inline'",
|
||||
"style-src": "'self' 'unsafe-inline'",
|
||||
"img-src": "'self' data:",
|
||||
"connect-src": "'self'",
|
||||
}
|
||||
if app.config.get("ENABLE_CSP", True)
|
||||
else None
|
||||
)
|
||||
|
||||
permissions_policy = {"browsing-topics": "()"} if app.config.get("ENABLE_PP") else None
|
||||
|
||||
@@ -424,9 +428,38 @@ def generate_new_receipt_filename(list_id):
|
||||
return f"list_{list_id}_{timestamp}_{random_part}.webp"
|
||||
|
||||
|
||||
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)
|
||||
|
||||
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()
|
||||
recalculate_filesizes(receipt.id)
|
||||
return {"success": True}
|
||||
except Exception as e:
|
||||
return {"success": False, "error": str(e)}
|
||||
|
||||
|
||||
def get_expenses_aggregated_by_list_created_at(
|
||||
user_only=False, admin=False, show_all=False,
|
||||
range_type="monthly", start_date=None, end_date=None, user_id=None
|
||||
user_only=False,
|
||||
admin=False,
|
||||
show_all=False,
|
||||
range_type="monthly",
|
||||
start_date=None,
|
||||
end_date=None,
|
||||
user_id=None,
|
||||
):
|
||||
"""
|
||||
Wspólna logika: sumujemy najnowszy wydatek z każdej listy,
|
||||
@@ -455,8 +488,7 @@ def get_expenses_aggregated_by_list_created_at(
|
||||
except Exception:
|
||||
return {"error": "Błędne daty", "labels": [], "expenses": []}
|
||||
lists_query = lists_query.filter(
|
||||
ShoppingList.created_at >= dt_start,
|
||||
ShoppingList.created_at < dt_end
|
||||
ShoppingList.created_at >= dt_start, ShoppingList.created_at < dt_end
|
||||
)
|
||||
lists = lists_query.all()
|
||||
|
||||
@@ -464,8 +496,7 @@ def get_expenses_aggregated_by_list_created_at(
|
||||
data = []
|
||||
for sl in lists:
|
||||
latest_exp = (
|
||||
Expense.query
|
||||
.filter_by(list_id=sl.id)
|
||||
Expense.query.filter_by(list_id=sl.id)
|
||||
.order_by(Expense.added_at.desc())
|
||||
.first()
|
||||
)
|
||||
@@ -1002,15 +1033,31 @@ def edit_my_list(list_id):
|
||||
abort(403, description="Nie jesteś właścicielem tej listy.")
|
||||
|
||||
if request.method == "POST":
|
||||
# Obsługa zmiany miesiąca utworzenia listy
|
||||
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)
|
||||
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(url_for("edit_my_list", list_id=list_id))
|
||||
except ValueError:
|
||||
flash("Nieprawidłowy format miesiąca", "danger")
|
||||
return redirect(url_for("edit_my_list", list_id=list_id))
|
||||
|
||||
# Pozostała aktualizacja pól
|
||||
new_title = request.form.get("title", "").strip()
|
||||
is_public = "is_public" in request.form
|
||||
is_temporary = "is_temporary" in request.form
|
||||
is_archived = "is_archived" in request.form
|
||||
|
||||
expires_date = request.form.get("expires_date")
|
||||
expires_time = request.form.get("expires_time")
|
||||
|
||||
# Walidacja tytułu
|
||||
if not new_title:
|
||||
flash("Podaj poprawny tytuł", "danger")
|
||||
return redirect(url_for("edit_my_list", list_id=list_id))
|
||||
@@ -1020,7 +1067,6 @@ def edit_my_list(list_id):
|
||||
l.is_temporary = is_temporary
|
||||
l.is_archived = is_archived
|
||||
|
||||
# Obsługa daty wygaśnięcia
|
||||
if expires_date and expires_time:
|
||||
try:
|
||||
combined = f"{expires_date} {expires_time}"
|
||||
@@ -1160,7 +1206,7 @@ def view_list(list_id):
|
||||
expenses=expenses,
|
||||
total_expense=total_expense,
|
||||
is_share=False,
|
||||
is_owner=is_owner
|
||||
is_owner=is_owner,
|
||||
)
|
||||
|
||||
|
||||
@@ -1254,7 +1300,7 @@ def user_expenses_data():
|
||||
range_type=range_type,
|
||||
start_date=start_date,
|
||||
end_date=end_date,
|
||||
user_id=current_user.id
|
||||
user_id=current_user.id,
|
||||
)
|
||||
if "error" in result:
|
||||
return jsonify({"error": result["error"]}), 400
|
||||
@@ -1545,6 +1591,22 @@ def analyze_receipts_for_list(list_id):
|
||||
return jsonify({"results": results, "total": round(total, 2)})
|
||||
|
||||
|
||||
@app.route("/user_crop_receipt", methods=["POST"])
|
||||
@login_required
|
||||
def crop_receipt_user():
|
||||
receipt_id = request.form.get("receipt_id")
|
||||
file = request.files.get("cropped_image")
|
||||
|
||||
receipt = Receipt.query.get_or_404(receipt_id)
|
||||
list_obj = ShoppingList.query.get_or_404(receipt.list_id)
|
||||
|
||||
if list_obj.owner_id != current_user.id and not current_user.is_admin:
|
||||
return jsonify(success=False, error="Brak dostępu"), 403
|
||||
|
||||
result = handle_crop_receipt(receipt_id, file)
|
||||
return jsonify(result)
|
||||
|
||||
|
||||
@app.route("/admin")
|
||||
@login_required
|
||||
@admin_required
|
||||
@@ -1775,8 +1837,11 @@ def admin_receipts(id):
|
||||
all_db_filenames = set(r.filename for r in receipts)
|
||||
files_on_disk = set(os.listdir(upload_folder))
|
||||
stale_files = [
|
||||
f for f in files_on_disk
|
||||
if f.endswith(".webp") and f not in all_db_filenames and f.startswith("list_")
|
||||
f
|
||||
for f in files_on_disk
|
||||
if f.endswith(".webp")
|
||||
and f not in all_db_filenames
|
||||
and f.startswith("list_")
|
||||
]
|
||||
else:
|
||||
list_id = int(id)
|
||||
@@ -1794,7 +1859,7 @@ def admin_receipts(id):
|
||||
"admin/receipts.html",
|
||||
receipts=receipts,
|
||||
orphan_files=stale_files,
|
||||
orphan_files_count=len(stale_files)
|
||||
orphan_files_count=len(stale_files),
|
||||
)
|
||||
|
||||
|
||||
@@ -1995,6 +2060,18 @@ def edit_list(list_id):
|
||||
flash("Niepoprawna kwota", "danger")
|
||||
return redirect(url_for("edit_list", list_id=list_id))
|
||||
|
||||
created_month = request.form.get("created_month")
|
||||
if created_month:
|
||||
try:
|
||||
year, month = map(int, created_month.split("-"))
|
||||
l.created_at = datetime(year, month, 1, tzinfo=timezone.utc)
|
||||
except ValueError:
|
||||
flash(
|
||||
"Nieprawidłowy format miesiąca (przeniesienie daty utworzenia)",
|
||||
"danger",
|
||||
)
|
||||
return redirect(url_for("edit_list", list_id=list_id))
|
||||
|
||||
db.session.add(l)
|
||||
db.session.commit()
|
||||
flash("Zapisano zmiany listy", "success")
|
||||
@@ -2232,31 +2309,11 @@ def demote_user(user_id):
|
||||
@app.route("/admin/crop_receipt", methods=["POST"])
|
||||
@login_required
|
||||
@admin_required
|
||||
def crop_receipt():
|
||||
def crop_receipt_admin():
|
||||
receipt_id = request.form.get("receipt_id")
|
||||
file = request.files.get("cropped_image")
|
||||
|
||||
if not receipt_id or not file:
|
||||
return jsonify(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)
|
||||
|
||||
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()
|
||||
recalculate_filesizes(receipt.id)
|
||||
return jsonify(success=True)
|
||||
except Exception as e:
|
||||
return jsonify(success=False, error=str(e))
|
||||
result = handle_crop_receipt(receipt_id, file)
|
||||
return jsonify(result)
|
||||
|
||||
|
||||
@app.route("/admin/recalculate_filesizes")
|
||||
|
@@ -4,22 +4,22 @@ 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;
|
||||
const image = document.getElementById("cropImage");
|
||||
image.src = imgSrc;
|
||||
|
||||
if (cropper) {
|
||||
cropper.destroy();
|
||||
cropper = null;
|
||||
}
|
||||
|
||||
cropImage.onload = () => {
|
||||
cropper = new Cropper(cropImage, {
|
||||
image.onload = () => {
|
||||
cropper = new Cropper(image, {
|
||||
viewMode: 1,
|
||||
autoCropArea: 1,
|
||||
responsive: true,
|
||||
@@ -36,51 +36,7 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
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;
|
||||
}
|
||||
|
||||
cropper.getCroppedCanvas().toBlob(function (blob) {
|
||||
const formData = new FormData();
|
||||
formData.append("receipt_id", currentReceiptId);
|
||||
formData.append("cropped_image", blob);
|
||||
@@ -91,7 +47,6 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
spinner.classList.add("d-none");
|
||||
if (data.success) {
|
||||
showToast("Zapisano przycięty paragon", "success");
|
||||
setTimeout(() => location.reload(), 1500);
|
||||
@@ -100,10 +55,9 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
spinner.classList.add("d-none");
|
||||
showToast("Błąd sieci", "danger");
|
||||
console.error(err);
|
||||
});
|
||||
}, "image/webp", 1.0);
|
||||
}, "image/webp");
|
||||
});
|
||||
});
|
||||
});
|
39
static/js/admin_receipt_crop.js
Normal file
39
static/js/admin_receipt_crop.js
Normal file
@@ -0,0 +1,39 @@
|
||||
(function () {
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
const cropModal = document.getElementById("adminCropModal");
|
||||
const cropImage = document.getElementById("adminCropImage");
|
||||
const spinner = document.getElementById("adminCropLoading");
|
||||
const saveButton = document.getElementById("adminSaveCrop");
|
||||
|
||||
if (!cropModal || !cropImage || !spinner || !saveButton) return;
|
||||
|
||||
let cropper;
|
||||
let currentReceiptId;
|
||||
const currentEndpoint = "/admin/crop_receipt";
|
||||
|
||||
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;
|
||||
|
||||
document.querySelectorAll('.cropper-container').forEach(e => e.remove());
|
||||
|
||||
if (cropper) cropper.destroy();
|
||||
cropImage.onload = () => {
|
||||
cropper = cropUtils.initCropper(cropImage);
|
||||
};
|
||||
});
|
||||
|
||||
cropModal.addEventListener("hidden.bs.modal", function () {
|
||||
cropUtils.cleanUpCropper(cropImage, cropper);
|
||||
cropper = null;
|
||||
});
|
||||
|
||||
saveButton.addEventListener("click", function () {
|
||||
if (!cropper) return;
|
||||
spinner.classList.remove("d-none");
|
||||
cropUtils.handleCrop(currentEndpoint, currentReceiptId, cropper, spinner);
|
||||
});
|
||||
});
|
||||
})();
|
74
static/js/receipt_crop_logic.js
Normal file
74
static/js/receipt_crop_logic.js
Normal file
@@ -0,0 +1,74 @@
|
||||
// receipt_crop_logic.js
|
||||
(function () {
|
||||
function initCropper(imgEl) {
|
||||
return new Cropper(imgEl, {
|
||||
viewMode: 1,
|
||||
autoCropArea: 1,
|
||||
responsive: true,
|
||||
background: false,
|
||||
zoomable: true,
|
||||
movable: true,
|
||||
dragMode: 'move',
|
||||
minContainerHeight: 400,
|
||||
minContainerWidth: 400,
|
||||
});
|
||||
}
|
||||
|
||||
function cleanUpCropper(imgEl, cropperInstance) {
|
||||
if (cropperInstance) {
|
||||
cropperInstance.destroy();
|
||||
}
|
||||
if (imgEl) imgEl.src = "";
|
||||
}
|
||||
|
||||
function handleCrop(endpoint, receiptId, cropper, spinner) {
|
||||
const croppedCanvas = cropper.getCroppedCanvas({
|
||||
imageSmoothingEnabled: false,
|
||||
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", receiptId);
|
||||
formData.append("cropped_image", blob);
|
||||
|
||||
fetch(endpoint, {
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
window.cropUtils = {
|
||||
initCropper,
|
||||
cleanUpCropper,
|
||||
handleCrop,
|
||||
};
|
||||
})();
|
39
static/js/user_receipt_crop.js
Normal file
39
static/js/user_receipt_crop.js
Normal file
@@ -0,0 +1,39 @@
|
||||
(function () {
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
const cropModal = document.getElementById("userCropModal");
|
||||
const cropImage = document.getElementById("userCropImage");
|
||||
const spinner = document.getElementById("userCropLoading");
|
||||
const saveButton = document.getElementById("userSaveCrop");
|
||||
|
||||
if (!cropModal || !cropImage || !spinner || !saveButton) return;
|
||||
|
||||
let cropper;
|
||||
let currentReceiptId;
|
||||
const currentEndpoint = "/user_crop_receipt";
|
||||
|
||||
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;
|
||||
|
||||
document.querySelectorAll('.cropper-container').forEach(e => e.remove());
|
||||
|
||||
if (cropper) cropper.destroy();
|
||||
cropImage.onload = () => {
|
||||
cropper = cropUtils.initCropper(cropImage);
|
||||
};
|
||||
});
|
||||
|
||||
cropModal.addEventListener("hidden.bs.modal", function () {
|
||||
cropUtils.cleanUpCropper(cropImage, cropper);
|
||||
cropper = null;
|
||||
});
|
||||
|
||||
saveButton.addEventListener("click", function () {
|
||||
if (!cropper) return;
|
||||
spinner.classList.remove("d-none");
|
||||
cropUtils.handleCrop(currentEndpoint, currentReceiptId, cropper, spinner);
|
||||
});
|
||||
});
|
||||
})();
|
@@ -65,6 +65,21 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Aktualna data utworzenia listy</label>
|
||||
<p class="form-control-plaintext text-white">
|
||||
{{ list.created_at.strftime('%Y-%m-%d') }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label for="created_month" class="form-label">Przenieś listę do miesiąca</label>
|
||||
<input type="month" id="created_month" name="created_month"
|
||||
class="form-control bg-dark text-white border-secondary rounded">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="mb-4">
|
||||
<label class="form-label">Link do udostępnienia</label>
|
||||
<input type="text" class="form-control bg-dark text-white border-secondary rounded" readonly
|
||||
|
@@ -36,8 +36,10 @@
|
||||
<a href="{{ url_for('rotate_receipt', 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="#cropModal" data-img-src="{{ url_for('uploaded_file', filename=r.filename) }}"
|
||||
data-receipt-id="{{ r.id }}">✂️ Przytnij</a>
|
||||
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-info w-100 mb-2">✏️
|
||||
Zmień nazwę</a>
|
||||
{% if not r.file_hash %}
|
||||
@@ -90,7 +92,7 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="modal fade" id="cropModal" tabindex="-1" aria-labelledby="cropModalLabel" aria-hidden="true">
|
||||
<div class="modal fade" id="adminCropModal" tabindex="-1" aria-labelledby="userCropModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-xl modal-dialog-centered">
|
||||
<div class="modal-content bg-dark text-white">
|
||||
<div class="modal-header">
|
||||
@@ -98,14 +100,12 @@
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div style="position: relative; width: 100%; height: 75vh;">
|
||||
<img id="cropImage" style="max-width: 100%; max-height: 100%; display: block; margin: auto;">
|
||||
<img id="adminCropImage" style="max-width: 100%; max-height: 100%; display: block; margin: auto;">
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-secondary" data-bs-dismiss="modal">Anuluj</button>
|
||||
<button class="btn btn-success" id="saveCrop">Zapisz</button>
|
||||
<div id="cropLoading" class="position-absolute top-50 start-50 translate-middle text-center d-none"
|
||||
style="z-index: 1055;">
|
||||
<button class="btn btn-success" id="adminSaveCrop">Zapisz</button>
|
||||
<div id="adminCropLoading" class="position-absolute top-50 start-50 translate-middle text-center d-none">
|
||||
<div class="spinner-border text-light" role="status"></div>
|
||||
<div class="mt-2 text-light">⏳ Pracuję...</div>
|
||||
</div>
|
||||
@@ -114,8 +114,12 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
{% block scripts %}
|
||||
<script src="{{ url_for('static_bp.serve_js', filename='receipt_crop.js') }}"></script>
|
||||
<script src="{{ url_for('static_bp.serve_js', filename='admin_receipt_crop.js') }}"></script>
|
||||
<script src="{{ url_for('static_bp.serve_js', filename='receipt_crop_logic.js') }}"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% endblock %}
|
@@ -52,7 +52,6 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
@@ -100,7 +99,7 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
{% if '/admin/' in request.path %}
|
||||
{% if '/admin/receipts' in request.path or '/edit_my_list' in request.path %}
|
||||
<script src="{{ url_for('static_bp.serve_js_lib', filename='cropper.min.js') }}"></script>
|
||||
{% endif %}
|
||||
|
||||
|
@@ -37,6 +37,21 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Aktualna data utworzenia:</label>
|
||||
<p class="form-control-plaintext text-white">
|
||||
{{ list.created_at.strftime('%Y-%m-%d') }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label for="move_to_month" class="form-label">Przenieś listę do miesiąca</label>
|
||||
<input type="month" id="move_to_month" name="move_to_month"
|
||||
class="form-control bg-dark text-white border-secondary rounded">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-check mb-3">
|
||||
<input class="form-check-input" type="checkbox" name="is_archived" id="is_archived" {% if list.is_archived
|
||||
%}checked{% endif %}>
|
||||
@@ -75,7 +90,11 @@
|
||||
|
||||
<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>
|
||||
@@ -116,14 +135,37 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form id="delete-form" method="post" action="{{ url_for('delete_user_list', list_id=list.id) }}"></form>
|
||||
|
||||
|
||||
<!-- Hidden delete form -->
|
||||
<form id="delete-form" method="post" action="{{ url_for('delete_user_list', list_id=list.id) }}"></form>
|
||||
|
||||
|
||||
<div class="modal fade" id="userCropModal" tabindex="-1" aria-labelledby="userCropModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-xl modal-dialog-centered">
|
||||
<div class="modal-content bg-dark text-white">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">✂️ Przycinanie paragonu</h5>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div style="position: relative; width: 100%; height: 75vh;">
|
||||
<img id="userCropImage" style="max-width: 100%; max-height: 100%; display: block; margin: auto;">
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-secondary" data-bs-dismiss="modal">Anuluj</button>
|
||||
<button class="btn btn-success" id="userSaveCrop">Zapisz</button>
|
||||
<div id="userCropLoading" class="position-absolute top-50 start-50 translate-middle text-center d-none">
|
||||
<div class="spinner-border text-light" role="status"></div>
|
||||
<div class="mt-2 text-light">⏳ Pracuję...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script src="{{ url_for('static_bp.serve_js', filename='confirm_delete.js') }}"></script>
|
||||
<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>
|
||||
{% endblock %}
|
Reference in New Issue
Block a user