diff --git a/app.py b/app.py
index d3e57a6..a670afc 100644
--- a/app.py
+++ b/app.py
@@ -45,7 +45,8 @@ from config import Config
from PIL import Image, ExifTags, ImageFilter, ImageOps
from werkzeug.utils import secure_filename
from werkzeug.middleware.proxy_fix import ProxyFix
-from sqlalchemy import func, extract, inspect
+from sqlalchemy import func, extract, inspect, or_
+from sqlalchemy.orm import joinedload
from collections import defaultdict, deque
from functools import wraps
@@ -114,7 +115,10 @@ class ShoppingList(db.Model):
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(150), nullable=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
+
owner_id = db.Column(db.Integer, db.ForeignKey("user.id"))
+ owner = db.relationship("User", backref="lists", foreign_keys=[owner_id])
+
is_temporary = db.Column(db.Boolean, default=False)
share_token = db.Column(db.String(64), unique=True, nullable=True)
# expires_at = db.Column(db.DateTime, nullable=True)
@@ -131,6 +135,8 @@ class Item(db.Model):
# added_at = db.Column(db.DateTime, default=datetime.utcnow)
added_at = db.Column(db.DateTime, default=utcnow)
added_by = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=True)
+ added_by_user = db.relationship("User", backref="added_items", lazy=True, foreign_keys=[added_by])
+
purchased = db.Column(db.Boolean, default=False)
purchased_at = db.Column(db.DateTime, nullable=True)
quantity = db.Column(db.Integer, default=1)
@@ -958,8 +964,7 @@ def view_list(list_id):
@app.route("/user_expenses")
@login_required
def user_expenses():
- from sqlalchemy.orm import joinedload
-
+ # Lista wydatków użytkownika
expenses = (
Expense.query.join(ShoppingList, Expense.list_id == ShoppingList.id)
.options(joinedload(Expense.list))
@@ -968,7 +973,7 @@ def user_expenses():
.all()
)
- rows = [
+ expense_table = [
{
"title": e.list.title if e.list else "Nieznana",
"amount": e.amount,
@@ -977,7 +982,35 @@ def user_expenses():
for e in expenses
]
- return render_template("user_expenses.html", expense_table=rows)
+ lists = (
+ ShoppingList.query
+ .filter(
+ or_(
+ ShoppingList.owner_id == current_user.id,
+ ShoppingList.is_public == True
+ )
+ )
+ .order_by(ShoppingList.created_at.desc())
+ .all()
+ )
+
+ lists_data = [
+ {
+ "id": l.id,
+ "title": l.title,
+ "created_at": l.created_at,
+ "total_expense": sum(e.amount for e in l.expenses),
+ "owner_username": l.owner.username if l.owner else "?"
+ }
+ for l in lists
+ ]
+
+
+ return render_template(
+ "user_expenses.html",
+ expense_table=expense_table,
+ lists_data=lists_data
+ )
@app.route("/user/expenses_data")
@@ -2213,6 +2246,10 @@ def handle_add_item(data):
name = data["name"].strip()
quantity = data.get("quantity", 1)
+ list_obj = db.session.get(ShoppingList, list_id)
+ if not list_obj:
+ return
+
try:
quantity = int(quantity)
if quantity < 1:
@@ -2248,12 +2285,15 @@ def handle_add_item(data):
if max_position is None:
max_position = 0
+ user_id = current_user.id if current_user.is_authenticated else None
+ user_name = current_user.username if current_user.is_authenticated else "Gość"
+
new_item = Item(
list_id=list_id,
name=name,
quantity=quantity,
position=max_position + 1,
- added_by=current_user.id if current_user.is_authenticated else None,
+ added_by=user_id,
)
db.session.add(new_item)
@@ -2271,9 +2311,9 @@ def handle_add_item(data):
"id": new_item.id,
"name": new_item.name,
"quantity": new_item.quantity,
- "added_by": (
- current_user.username if current_user.is_authenticated else "Gość"
- ),
+ "added_by": user_name,
+ "added_by_id": user_id,
+ "owner_id": list_obj.owner_id,
},
to=str(list_id),
include_self=True,
@@ -2292,6 +2332,7 @@ def handle_add_item(data):
)
+
@socketio.on("check_item")
def handle_check_item(data):
# item = Item.query.get(data["item_id"])
@@ -2345,7 +2386,19 @@ def handle_uncheck_item(data):
@socketio.on("request_full_list")
def handle_request_full_list(data):
list_id = data["list_id"]
- items = Item.query.filter_by(list_id=list_id).order_by(Item.position.asc()).all()
+
+ shopping_list = db.session.get(ShoppingList, list_id)
+ if not shopping_list:
+ return
+
+ owner_id = shopping_list.owner_id
+
+ items = (
+ Item.query.options(joinedload(Item.added_by_user))
+ .filter_by(list_id=list_id)
+ .order_by(Item.position.asc())
+ .all()
+ )
items_data = []
for item in items:
@@ -2358,12 +2411,16 @@ def handle_request_full_list(data):
"not_purchased": item.not_purchased,
"not_purchased_reason": item.not_purchased_reason,
"note": item.note or "",
+ "added_by": item.added_by_user.username if item.added_by_user else None,
+ "added_by_id": item.added_by_user.id if item.added_by_user else None,
+ "owner_id": owner_id,
}
)
emit("full_list", {"items": items_data}, to=request.sid)
+
@socketio.on("update_note")
def handle_update_note(data):
item_id = data["item_id"]
diff --git a/static/js/functions.js b/static/js/functions.js
index 6df9da8..664c5ff 100644
--- a/static/js/functions.js
+++ b/static/js/functions.js
@@ -272,8 +272,98 @@ function isListDifferent(oldItems, newItems) {
return false;
}
-function updateListSmoothly(newItems) {
+function renderItem(item, isShare = window.IS_SHARE, showEditOnly = false) {
+ const li = document.createElement('li');
+ li.id = `item-${item.id}`;
+ li.dataset.name = item.name.toLowerCase();
+ li.className = `list-group-item d-flex justify-content-between align-items-center flex-wrap clickable-item ${item.purchased ? 'bg-success text-white'
+ : item.not_purchased ? 'bg-warning text-dark'
+ : 'item-not-checked'
+ }`;
+
+ let quantityBadge = '';
+ if (item.quantity && item.quantity > 1) {
+ quantityBadge = `x${item.quantity} `;
+ }
+
+ let checkboxOrIcon = item.not_purchased
+ ? `🚫 `
+ : ` `;
+
+ let noteHTML = item.note
+ ? `[ ${item.note} ] ` : '';
+
+ let reasonHTML = item.not_purchased_reason
+ ? `[ Powód: ${item.not_purchased_reason} ] ` : '';
+
+ let left = `
+
+ ${window.isSorting ? `☰ ` : ''}
+ ${checkboxOrIcon}
+ ${item.name} ${quantityBadge}
+ ${noteHTML}
+ ${reasonHTML}
+
`;
+
+ let rightButtons = '';
+
+ // ✏️ i 🗑️ — tylko jeśli nie jesteśmy w trybie /share lub jesteśmy w 15s (tymczasowo)
+ if (!isShare || showEditOnly) {
+ rightButtons += `
+
+ ✏️
+
+
+ 🗑️
+ `;
+ }
+
+ // ✅ Jeśli element jest oznaczony jako niekupiony — pokaż "Przywróć"
+ if (item.not_purchased) {
+ rightButtons += `
+
+ ✅ Przywróć
+ `;
+ }
+
+ // ⚠️ tylko jeśli NIE jest oznaczony jako niekupiony i nie jesteśmy w 15s
+ if (!item.not_purchased && !showEditOnly) {
+ rightButtons += `
+
+ ⚠️
+ `;
+ }
+
+ // 📝 tylko jeśli jesteśmy w /share i nie jesteśmy w 15s
+ if (isShare && !showEditOnly) {
+ rightButtons += `
+
+ 📝
+ `;
+ }
+
+
+ li.innerHTML = `${left}${rightButtons}
`;
+
+ if (item.added_by && item.owner_id && item.added_by_id && item.added_by_id !== item.owner_id) {
+ const infoEl = document.createElement('small');
+ infoEl.className = 'text-info ms-4';
+ infoEl.innerHTML = `[Dodane przez: ${item.added_by} ]`;
+ li.querySelector('.d-flex.align-items-center')?.appendChild(infoEl);
+ }
+
+ return li;
+}
+
+
+
+function updateListSmoothly(newItems) {
const itemsContainer = document.getElementById('items');
const existingItemsMap = new Map();
@@ -285,68 +375,7 @@ function updateListSmoothly(newItems) {
const fragment = document.createDocumentFragment();
newItems.forEach(item => {
- let li = existingItemsMap.get(item.id);
- let quantityBadge = '';
- if (item.quantity && item.quantity > 1) {
- quantityBadge = `x${item.quantity} `;
- }
-
- if (!li) {
- li = document.createElement('li');
- li.id = `item-${item.id}`;
- }
-
- // Klasy tła
- li.className = `list-group-item d-flex justify-content-between align-items-center flex-wrap clickable-item ${item.purchased ? 'bg-success text-white' :
- item.not_purchased ? 'bg-warning text-dark' : 'item-not-checked'
- }`;
-
- // Wewnętrzny HTML
- li.innerHTML = `
-
- ${isSorting ? `☰ ` : ''}
- ${!item.not_purchased ? `
-
- ` : `
- 🚫
- `}
- ${item.name} ${quantityBadge}
-
- ${item.note ? `[ ${item.note} ] ` : ''}
- ${item.not_purchased_reason ? `[ Powód: ${item.not_purchased_reason} ] ` : ''}
-
-
- ${item.not_purchased ? `
-
- ✅ Przywróć
-
- ` : `
-
- ⚠️
-
- ${window.IS_SHARE ? `
-
- 📝
-
- ` : ''}
- `}
- ${!window.IS_SHARE ? `
-
- ✏️
-
-
- 🗑️
-
- ` : ''}
-
- `;
-
+ const li = renderItem(item);
fragment.appendChild(li);
});
diff --git a/static/js/live.js b/static/js/live.js
index e96ebdb..4f13c51 100644
--- a/static/js/live.js
+++ b/static/js/live.js
@@ -127,69 +127,59 @@ function setupList(listId, username) {
showToast(`Dodano wydatek: ${data.amount.toFixed(2)} PLN`, 'info');
});
+
socket.on('item_added', data => {
showToast(`${data.added_by} dodał: ${data.name}`, 'info');
- const li = document.createElement('li');
- li.className = 'list-group-item d-flex justify-content-between align-items-center flex-wrap item-not-checked';
- li.id = `item-${data.id}`;
- let quantityBadge = '';
- if (data.quantity && data.quantity > 1) {
- quantityBadge = `x${data.quantity} `;
- }
-
- const countdownId = `countdown-${data.id}`;
- const countdownBtn = `
- 15s
- `;
-
- li.innerHTML = `
-
-
-
- ${data.name} ${quantityBadge}
-
-
-
- ${countdownBtn}
-
- ✏️
-
-
- 🗑️
-
-
- `;
+ const item = {
+ ...data,
+ purchased: false,
+ not_purchased: false,
+ not_purchased_reason: '',
+ note: ''
+ };
+ const li = renderItem(item, false, true); // ← tryb 15s
document.getElementById('items').appendChild(li);
toggleEmptyPlaceholder();
+ updateProgressBar();
- // ⏳ Licznik odliczania
- let seconds = 15;
- const countdownEl = document.getElementById(countdownId);
- const intervalId = setInterval(() => {
- seconds--;
- if (countdownEl) {
- countdownEl.textContent = `${seconds}s`;
- }
- if (seconds <= 0) {
- clearInterval(intervalId);
- if (countdownEl) countdownEl.remove();
- }
- }, 1000);
+ if (window.IS_SHARE) {
+ const countdownId = `countdown-${data.id}`;
+ const countdownBtn = document.createElement('button');
+ countdownBtn.type = 'button';
+ countdownBtn.className = 'btn btn-outline-warning';
+ countdownBtn.id = countdownId;
+ countdownBtn.disabled = true;
+ countdownBtn.textContent = '15s';
- // 🔁 Request listy po 15s
- setTimeout(() => {
- if (window.LIST_ID) {
- socket.emit('request_full_list', { list_id: window.LIST_ID });
- }
- }, 15000);
+ li.querySelector('.btn-group')?.prepend(countdownBtn);
+
+ let seconds = 15;
+ const intervalId = setInterval(() => {
+ const el = document.getElementById(countdownId);
+ if (el) {
+ seconds--;
+ el.textContent = `${seconds}s`;
+ if (seconds <= 0) {
+ el.remove();
+ clearInterval(intervalId);
+ }
+ } else {
+ clearInterval(intervalId);
+ }
+ }, 1000);
+
+ setTimeout(() => {
+ const existing = document.getElementById(`item-${data.id}`);
+ if (existing) {
+ const updated = renderItem(item, true);
+ existing.replaceWith(updated);
+ }
+ }, 15000);
+ }
});
-
-
socket.on('item_deleted', data => {
const li = document.getElementById(`item-${data.item_id}`);
if (li) {
diff --git a/static/js/user_expense_lists.js b/static/js/user_expense_lists.js
new file mode 100644
index 0000000..fa6cdd9
--- /dev/null
+++ b/static/js/user_expense_lists.js
@@ -0,0 +1,103 @@
+document.addEventListener('DOMContentLoaded', () => {
+ const checkboxes = document.querySelectorAll('.list-checkbox');
+ const totalEl = document.getElementById('listsTotal');
+ const filterButtons = document.querySelectorAll('.filter-btn');
+ const rows = document.querySelectorAll('#listsTableBody tr');
+
+ const onlyWith = document.getElementById('onlyWithExpenses');
+ const customStart = document.getElementById('customStart');
+ const customEnd = document.getElementById('customEnd');
+
+ function updateTotal() {
+ let total = 0;
+ checkboxes.forEach(cb => {
+ const row = cb.closest('tr');
+ if (cb.checked && row.style.display !== 'none') {
+ total += parseFloat(cb.dataset.amount);
+ }
+ });
+
+ totalEl.textContent = total.toFixed(2) + ' PLN';
+ totalEl.parentElement.classList.add('animate__animated', 'animate__fadeIn');
+ setTimeout(() => {
+ totalEl.parentElement.classList.remove('animate__animated', 'animate__fadeIn');
+ }, 400);
+ }
+
+ checkboxes.forEach(cb => cb.addEventListener('change', updateTotal));
+
+ filterButtons.forEach(btn => {
+ btn.addEventListener('click', () => {
+ filterButtons.forEach(b => b.classList.remove('active'));
+ btn.classList.add('active');
+ const range = btn.dataset.range;
+
+ const now = new Date();
+ const todayStr = now.toISOString().slice(0, 10);
+ const year = now.getFullYear();
+ const month = now.toISOString().slice(0, 7);
+ const week = `${year}-${String(getISOWeek(now)).padStart(2, '0')}`;
+
+ rows.forEach(row => {
+ const rDate = row.dataset.date;
+ const rMonth = row.dataset.month;
+ const rWeek = row.dataset.week;
+ const rYear = row.dataset.year;
+
+ let show = true;
+ if (range === 'day') show = rDate === todayStr;
+ if (range === 'month') show = rMonth === month;
+ if (range === 'week') show = rWeek === week;
+ if (range === 'year') show = rYear === String(year);
+
+ row.style.display = show ? '' : 'none';
+ });
+
+ applyExpenseFilter();
+ updateTotal();
+ });
+ });
+
+ // ISO week helper
+ function getISOWeek(date) {
+ const target = new Date(date.valueOf());
+ const dayNr = (date.getDay() + 6) % 7;
+ target.setDate(target.getDate() - dayNr + 3);
+ const firstThursday = new Date(target.getFullYear(), 0, 4);
+ const dayDiff = (target - firstThursday) / 86400000;
+ return 1 + Math.floor(dayDiff / 7);
+ }
+
+ // Zakres dat: kliknij „Zastosuj zakres”
+ document.getElementById('applyCustomRange').addEventListener('click', () => {
+ const start = customStart.value;
+ const end = customEnd.value;
+
+ filterButtons.forEach(b => b.classList.remove('active'));
+
+ rows.forEach(row => {
+ const date = row.dataset.date;
+ const show = (!start || date >= start) && (!end || date <= end);
+ row.style.display = show ? '' : 'none';
+ });
+
+ applyExpenseFilter();
+ updateTotal();
+ });
+
+ // Filtrowanie tylko list z wydatkami
+ if (onlyWith) {
+ onlyWith.addEventListener('change', () => {
+ applyExpenseFilter();
+ updateTotal();
+ });
+ }
+
+ function applyExpenseFilter() {
+ if (!onlyWith || !onlyWith.checked) return;
+ rows.forEach(row => {
+ const amt = parseFloat(row.querySelector('.list-checkbox').dataset.amount || 0);
+ if (amt <= 0) row.style.display = 'none';
+ });
+ }
+});
diff --git a/templates/list.html b/templates/list.html
index 7a69337..c1830d1 100644
--- a/templates/list.html
+++ b/templates/list.html
@@ -131,11 +131,11 @@
{% endif %}
{% if not is_share %}
-
✏️
-
🗑️
diff --git a/templates/user_expenses.html b/templates/user_expenses.html
index d351519..2623592 100644
--- a/templates/user_expenses.html
+++ b/templates/user_expenses.html
@@ -10,12 +10,14 @@
+
-
- 📄 Tabela
+ 📚 Listy
+
@@ -25,32 +27,77 @@
-
-
+
+
+
- {% if expense_table %}
-
- {% for row in expense_table %}
-
-
-
-
{{ row.title }}
-
💸 {{ '%.2f'|format(row.amount) }} PLN
-
📅 {{ row.added_at.strftime('%Y-%m-%d') }}
-
-
-
- {% endfor %}
+
+
+ Wszystko
+ 🗓️ Dzień
+ 📆 Tydzień
+ 📅 Miesiąc
+ 📈 Rok
- {% else %}
-
Brak wydatków do wyświetlenia.
- {% endif %}
+
+
+
+
+
+
+
+
+
+ 📊 Zastosuj zakres
+
+
+
+
+ Pokaż tylko listy z wydatkami
+
+
+
+
+
💰 Suma zaznaczonych: 0.00 PLN
-
+
+
+
@@ -86,4 +134,5 @@
{% block scripts %}
+
{% endblock %}
\ No newline at end of file