diff --git a/app.py b/app.py
index 6014b47..907bab3 100644
--- a/app.py
+++ b/app.py
@@ -295,7 +295,9 @@ def save_resized_image(file, path):
image.info.clear()
new_path = path.rsplit(".", 1)[0] + ".webp"
- image.save(new_path, format="WEBP", quality=100, method=0)
+ # image.save(new_path, format="WEBP", quality=100, method=0)
+ image.save(new_path, format="WEBP", lossless=True, method=6)
+
except Exception as e:
raise ValueError(f"Błąd podczas przetwarzania obrazu: {e}")
@@ -662,13 +664,34 @@ def favicon():
@app.route("/")
def main_page():
- # now = datetime.utcnow()
now = datetime.now(timezone.utc)
+ month_str = request.args.get("month")
+ start = end = None
+
+ if month_str:
+ try:
+ year, month = map(int, month_str.split("-"))
+ start = datetime(year, month, 1, tzinfo=timezone.utc)
+ end = (start + timedelta(days=31)).replace(day=1)
+ except:
+ start = end = None
+
+ def date_filter(query):
+ if start and end:
+ query = query.filter(
+ ShoppingList.created_at >= start, ShoppingList.created_at < end
+ )
+ return query
if current_user.is_authenticated:
user_lists = (
- ShoppingList.query.filter_by(owner_id=current_user.id, is_archived=False)
- .filter((ShoppingList.expires_at == None) | (ShoppingList.expires_at > now))
+ date_filter(
+ ShoppingList.query.filter_by(
+ owner_id=current_user.id, is_archived=False
+ ).filter(
+ (ShoppingList.expires_at == None) | (ShoppingList.expires_at > now)
+ )
+ )
.order_by(ShoppingList.created_at.desc())
.all()
)
@@ -680,11 +703,16 @@ def main_page():
)
public_lists = (
- ShoppingList.query.filter(
- ShoppingList.is_public == True,
- ShoppingList.owner_id != current_user.id,
- ((ShoppingList.expires_at == None) | (ShoppingList.expires_at > now)),
- ShoppingList.is_archived == False,
+ date_filter(
+ ShoppingList.query.filter(
+ ShoppingList.is_public == True,
+ ShoppingList.owner_id != current_user.id,
+ (
+ (ShoppingList.expires_at == None)
+ | (ShoppingList.expires_at > now)
+ ),
+ ShoppingList.is_archived == False,
+ )
)
.order_by(ShoppingList.created_at.desc())
.all()
@@ -693,10 +721,15 @@ def main_page():
user_lists = []
archived_lists = []
public_lists = (
- ShoppingList.query.filter(
- ShoppingList.is_public == True,
- ((ShoppingList.expires_at == None) | (ShoppingList.expires_at > now)),
- ShoppingList.is_archived == False,
+ date_filter(
+ ShoppingList.query.filter(
+ ShoppingList.is_public == True,
+ (
+ (ShoppingList.expires_at == None)
+ | (ShoppingList.expires_at > now)
+ ),
+ ShoppingList.is_archived == False,
+ )
)
.order_by(ShoppingList.created_at.desc())
.all()
@@ -710,6 +743,8 @@ def main_page():
user_lists=user_lists,
public_lists=public_lists,
archived_lists=archived_lists,
+ now=now,
+ timedelta=timedelta,
)
@@ -1028,16 +1063,24 @@ def user_expenses():
)
-@app.route("/user/expenses_data")
+@app.route("/user_expenses_data")
@login_required
def user_expenses_data():
range_type = request.args.get("range", "monthly")
start_date = request.args.get("start_date")
end_date = request.args.get("end_date")
+ show_all = request.args.get("show_all", "false").lower() == "true"
- query = Expense.query.join(ShoppingList, Expense.list_id == ShoppingList.id).filter(
- ShoppingList.owner_id == current_user.id
- )
+ query = Expense.query.join(ShoppingList, Expense.list_id == ShoppingList.id)
+
+ if show_all:
+ query = query.filter(
+ or_(
+ ShoppingList.owner_id == current_user.id, ShoppingList.is_public == True
+ )
+ )
+ else:
+ query = query.filter(ShoppingList.owner_id == current_user.id)
if start_date and end_date:
try:
@@ -2110,15 +2153,15 @@ def crop_receipt():
old_path = os.path.join(app.config["UPLOAD_FOLDER"], receipt.filename)
try:
- image = Image.open(file).convert("RGB")
new_filename = generate_new_receipt_filename(receipt.list_id)
new_path = os.path.join(app.config["UPLOAD_FOLDER"], new_filename)
- image.save(new_path, format="WEBP", quality=100)
+
+ save_resized_image(file, new_path)
if os.path.exists(old_path):
os.remove(old_path)
- receipt.filename = new_filename
+ receipt.filename = os.path.basename(new_path)
db.session.commit()
return jsonify(success=True)
diff --git a/static/css/style.css b/static/css/style.css
index ee72e62..d7e1dea 100644
--- a/static/css/style.css
+++ b/static/css/style.css
@@ -205,7 +205,6 @@ input.form-control {
box-shadow: 0 -1px 4px rgba(0, 0, 0, 0.25);
}
-
@media (max-width: 768px) {
.info-bar-fixed {
position: static;
@@ -310,4 +309,12 @@ input.form-control {
.only-mobile {
display: none !important;
}
+}
+
+.sorting-active {
+ border: 2px dashed #ffc107;
+ border-radius: 0.5rem;
+ background-color: rgba(255, 193, 7, 0.05);
+ padding: 0.5rem;
+ transition: border 0.3s, background-color 0.3s;
}
\ No newline at end of file
diff --git a/static/js/functions.js b/static/js/functions.js
index fcabe77..1154a86 100644
--- a/static/js/functions.js
+++ b/static/js/functions.js
@@ -272,7 +272,6 @@ function isListDifferent(oldItems, newItems) {
return false;
}
-
function renderItem(item, isShare = window.IS_SHARE, showEditOnly = false) {
const li = document.createElement('li');
li.id = `item-${item.id}`;
@@ -297,9 +296,11 @@ function renderItem(item, isShare = window.IS_SHARE, showEditOnly = false) {
let reasonHTML = item.not_purchased_reason
? `[ Powód: ${item.not_purchased_reason} ]` : '';
+ let dragHandle = window.isSorting ? `☰` : '';
+
let left = `
- ${window.isSorting ? `☰` : ''}
+ ${dragHandle}
${checkboxOrIcon}
${item.name} ${quantityBadge}
${noteHTML}
@@ -361,8 +362,6 @@ function renderItem(item, isShare = window.IS_SHARE, showEditOnly = false) {
return li;
}
-
-
function updateListSmoothly(newItems) {
const itemsContainer = document.getElementById('items');
const existingItemsMap = new Map();
diff --git a/static/js/receipt_crop.js b/static/js/receipt_crop.js
index 94bf38d..33754a0 100644
--- a/static/js/receipt_crop.js
+++ b/static/js/receipt_crop.js
@@ -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");
- const image = document.getElementById("cropImage");
- image.src = imgSrc;
+ cropImage.src = imgSrc;
if (cropper) {
cropper.destroy();
cropper = null;
}
- image.onload = () => {
- cropper = new Cropper(image, {
+ cropImage.onload = () => {
+ cropper = new Cropper(cropImage, {
viewMode: 1,
autoCropArea: 1,
responsive: true,
@@ -36,7 +36,51 @@ document.addEventListener("DOMContentLoaded", function () {
document.getElementById("saveCrop").addEventListener("click", function () {
if (!cropper) return;
- cropper.getCroppedCanvas().toBlob(function (blob) {
+ 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);
@@ -47,6 +91,7 @@ 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);
@@ -55,9 +100,10 @@ document.addEventListener("DOMContentLoaded", function () {
}
})
.catch((err) => {
+ spinner.classList.add("d-none");
showToast("Błąd sieci", "danger");
console.error(err);
});
- }, "image/webp");
+ }, "image/webp", 1.0);
});
});
diff --git a/static/js/select_month.js b/static/js/select_month.js
new file mode 100644
index 0000000..8a7f958
--- /dev/null
+++ b/static/js/select_month.js
@@ -0,0 +1,14 @@
+document.addEventListener("DOMContentLoaded", () => {
+ const select = document.getElementById("monthSelect");
+ if (!select) return;
+ select.addEventListener("change", () => {
+ const month = select.value;
+ const url = new URL(window.location.href);
+ if (month) {
+ url.searchParams.set("month", month);
+ } else {
+ url.searchParams.delete("month");
+ }
+ window.location.href = url.toString();
+ });
+});
\ No newline at end of file
diff --git a/static/js/sort_mode.js b/static/js/sort_mode.js
index 0a91843..fad6e84 100644
--- a/static/js/sort_mode.js
+++ b/static/js/sort_mode.js
@@ -2,53 +2,54 @@ let sortable = null;
let isSorting = false;
function enableSortMode() {
- if (sortable || isSorting) return;
+ if (isSorting) return;
isSorting = true;
+ window.isSorting = true;
localStorage.setItem('sortModeEnabled', 'true');
const itemsContainer = document.getElementById('items');
const listId = window.LIST_ID;
-
if (!itemsContainer || !listId) return;
- sortable = Sortable.create(itemsContainer, {
- animation: 150,
- handle: '.drag-handle',
- ghostClass: 'drag-ghost',
- filter: 'input, button',
- preventOnFilter: false,
- onEnd: function () {
- const order = Array.from(itemsContainer.children)
- .map(li => parseInt(li.id.replace('item-', '')))
- .filter(id => !isNaN(id));
-
- fetch('/reorder_items', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ list_id: listId, order })
- }).then(() => {
- showToast('Zapisano nową kolejność', 'success');
-
- if (window.currentItems) {
- window.currentItems = order.map(id =>
- window.currentItems.find(item => item.id === id)
- );
- updateListSmoothly(window.currentItems);
- }
- });
- }
- });
-
- const btn = document.getElementById('sort-toggle-btn');
- if (btn) {
- btn.textContent = '✔️ Zakończ sortowanie';
- btn.classList.remove('btn-outline-warning');
- btn.classList.add('btn-outline-success');
- }
-
+ // Odśwież widok listy z uchwytami (☰)
if (window.currentItems) {
updateListSmoothly(window.currentItems);
}
+
+ // Poczekaj na DOM po odświeżeniu listy
+ setTimeout(() => {
+ if (sortable) sortable.destroy();
+
+ sortable = Sortable.create(itemsContainer, {
+ animation: 150,
+ handle: '.drag-handle',
+ ghostClass: 'drag-ghost',
+ filter: 'input, button',
+ preventOnFilter: false,
+ onEnd: () => {
+ const order = Array.from(itemsContainer.children)
+ .map(li => parseInt(li.id.replace('item-', '')))
+ .filter(id => !isNaN(id));
+
+ fetch('/reorder_items', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ list_id: listId, order })
+ }).then(() => {
+ showToast('Zapisano nową kolejność', 'success');
+
+ if (window.currentItems) {
+ window.currentItems = order.map(id =>
+ window.currentItems.find(item => item.id === id)
+ );
+ updateListSmoothly(window.currentItems);
+ }
+ });
+ }
+ });
+
+ updateSortButtonUI(true);
+ }, 50);
}
function disableSortMode() {
@@ -56,28 +57,40 @@ function disableSortMode() {
sortable.destroy();
sortable = null;
}
+
isSorting = false;
localStorage.removeItem('sortModeEnabled');
-
- const btn = document.getElementById('sort-toggle-btn');
- if (btn) {
- btn.textContent = '✳️ Zmień kolejność';
- btn.classList.remove('btn-outline-success');
- btn.classList.add('btn-outline-warning');
- }
-
+ window.isSorting = false;
if (window.currentItems) {
updateListSmoothly(window.currentItems);
}
+
+ updateSortButtonUI(false);
+
}
function toggleSortMode() {
isSorting ? disableSortMode() : enableSortMode();
}
+function updateSortButtonUI(active) {
+ const btn = document.getElementById('sort-toggle-btn');
+ if (!btn) return;
+
+ if (active) {
+ btn.textContent = '✔️ Zakończ sortowanie';
+ btn.classList.remove('btn-outline-warning');
+ btn.classList.add('btn-outline-success');
+ } else {
+ btn.textContent = '✳️ Zmień kolejność';
+ btn.classList.remove('btn-outline-success');
+ btn.classList.add('btn-outline-warning');
+ }
+}
+
document.addEventListener('DOMContentLoaded', () => {
const wasSorting = localStorage.getItem('sortModeEnabled') === 'true';
if (wasSorting) {
enableSortMode();
}
-});
\ No newline at end of file
+});
diff --git a/static/js/user_expenses.js b/static/js/user_expenses.js
index a029e70..437dcfa 100644
--- a/static/js/user_expenses.js
+++ b/static/js/user_expenses.js
@@ -3,7 +3,11 @@ document.addEventListener("DOMContentLoaded", function () {
const rangeLabel = document.getElementById("chartRangeLabel");
function loadExpenses(range = "monthly", startDate = null, endDate = null) {
- let url = '/user/expenses_data?range=' + range;
+ let url = '/user_expenses_data?range=' + range;
+ const showAllCheckbox = document.getElementById("showAllLists");
+ if (showAllCheckbox && showAllCheckbox.checked) {
+ url += '&show_all=true';
+ }
if (startDate && endDate) {
url += `&start_date=${startDate}&end_date=${endDate}`;
}
diff --git a/templates/admin/receipts.html b/templates/admin/receipts.html
index 736a512..3361b37 100644
--- a/templates/admin/receipts.html
+++ b/templates/admin/receipts.html
@@ -78,6 +78,11 @@
diff --git a/templates/main.html b/templates/main.html
index 1891bc7..1cce537 100644
--- a/templates/main.html
+++ b/templates/main.html
@@ -31,6 +31,33 @@
{% endif %}
+{% set month_names = ["styczeń", "luty", "marzec", "kwiecień", "maj", "czerwiec", "lipiec", "sierpień", "wrzesień",
+"październik", "listopad", "grudzień"] %}
+{% set selected_month = request.args.get('month') or now.strftime('%Y-%m') %}
+
+
+
+
+
+
+
+
+
+
+
+
{% if current_user.is_authenticated %}
Twoje listy
@@ -78,8 +105,7 @@
{% endfor %}
{% else %}
-
Nie masz jeszcze żadnych list. Utwórz pierwszą, korzystając
- z formularza powyżej
+Nie utworzono żadnej listy
{% endif %}
{% endif %}
@@ -114,7 +140,6 @@
Brak dostępnych list publicznych do wyświetlenia
{% endif %}
-
+
+
{% block scripts %}
+
{% endblock %}
{% endblock %}
\ No newline at end of file
diff --git a/templates/user_expenses.html b/templates/user_expenses.html
index 17f5852..f991dd6 100644
--- a/templates/user_expenses.html
+++ b/templates/user_expenses.html
@@ -6,7 +6,11 @@
Statystyki wydatków
← Powrót
-
+
+
+
+
@@ -47,11 +51,7 @@
-
-
-
-
+
Od