commit #1
This commit is contained in:
77
app.py
77
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"]
|
||||
|
@@ -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 = `<span class="badge bg-secondary">x${item.quantity}</span>`;
|
||||
}
|
||||
|
||||
let checkboxOrIcon = item.not_purchased
|
||||
? `<span class="ms-1 block-icon">🚫</span>`
|
||||
: `<input id="checkbox-${item.id}" class="large-checkbox" type="checkbox" ${item.purchased ? 'checked' : ''}>`;
|
||||
|
||||
let noteHTML = item.note
|
||||
? `<small class="text-danger ms-4">[ <b>${item.note}</b> ]</small>` : '';
|
||||
|
||||
let reasonHTML = item.not_purchased_reason
|
||||
? `<small class="text-dark ms-4">[ <b>Powód: ${item.not_purchased_reason}</b> ]</small>` : '';
|
||||
|
||||
let left = `
|
||||
<div class="d-flex align-items-center gap-2 flex-grow-1">
|
||||
${window.isSorting ? `<span class="drag-handle me-2 text-danger" style="cursor: grab;">☰</span>` : ''}
|
||||
${checkboxOrIcon}
|
||||
<span id="name-${item.id}" class="text-white">${item.name} ${quantityBadge}</span>
|
||||
${noteHTML}
|
||||
${reasonHTML}
|
||||
</div>`;
|
||||
|
||||
let rightButtons = '';
|
||||
|
||||
// ✏️ i 🗑️ — tylko jeśli nie jesteśmy w trybie /share lub jesteśmy w 15s (tymczasowo)
|
||||
if (!isShare || showEditOnly) {
|
||||
rightButtons += `
|
||||
<button type="button" class="btn btn-outline-light"
|
||||
onclick="editItem(${item.id}, '${item.name.replace(/'/g, "\\'")}', ${item.quantity || 1})">
|
||||
✏️
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-light"
|
||||
onclick="deleteItem(${item.id})">
|
||||
🗑️
|
||||
</button>`;
|
||||
}
|
||||
|
||||
// ✅ Jeśli element jest oznaczony jako niekupiony — pokaż "Przywróć"
|
||||
if (item.not_purchased) {
|
||||
rightButtons += `
|
||||
<button type="button" class="btn btn-outline-light me-auto"
|
||||
onclick="unmarkNotPurchased(${item.id})">
|
||||
✅ Przywróć
|
||||
</button>`;
|
||||
}
|
||||
|
||||
// ⚠️ tylko jeśli NIE jest oznaczony jako niekupiony i nie jesteśmy w 15s
|
||||
if (!item.not_purchased && !showEditOnly) {
|
||||
rightButtons += `
|
||||
<button type="button" class="btn btn-outline-light"
|
||||
onclick="markNotPurchasedModal(event, ${item.id})">
|
||||
⚠️
|
||||
</button>`;
|
||||
}
|
||||
|
||||
// 📝 tylko jeśli jesteśmy w /share i nie jesteśmy w 15s
|
||||
if (isShare && !showEditOnly) {
|
||||
rightButtons += `
|
||||
<button type="button" class="btn btn-outline-light"
|
||||
onclick="openNoteModal(event, ${item.id})">
|
||||
📝
|
||||
</button>`;
|
||||
}
|
||||
|
||||
|
||||
li.innerHTML = `${left}<div class="btn-group btn-group-sm" role="group">${rightButtons}</div>`;
|
||||
|
||||
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: <b>${item.added_by}</b>]`;
|
||||
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 = `<span class="badge bg-secondary">x${item.quantity}</span>`;
|
||||
}
|
||||
|
||||
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 = `
|
||||
<div class="d-flex align-items-center gap-2 flex-grow-1">
|
||||
${isSorting ? `<span class="drag-handle me-2 text-danger" style="cursor: grab;">☰</span>` : ''}
|
||||
${!item.not_purchased ? `
|
||||
<input id="checkbox-${item.id}" class="large-checkbox" type="checkbox"
|
||||
${item.purchased ? 'checked' : ''}>
|
||||
` : `
|
||||
<span class="ms-1 block-icon">🚫</span>
|
||||
`}
|
||||
<span id="name-${item.id}" class="text-white">${item.name} ${quantityBadge}</span>
|
||||
|
||||
${item.note ? `<small class="text-danger ms-4">[ <b>${item.note}</b> ]</small>` : ''}
|
||||
${item.not_purchased_reason ? `<small class="text-dark ms-4">[ <b>Powód: ${item.not_purchased_reason}</b> ]</small>` : ''}
|
||||
</div>
|
||||
<div class="btn-group btn-group-sm" role="group">
|
||||
${item.not_purchased ? `
|
||||
<button type="button" class="btn btn-outline-light me-auto"
|
||||
onclick="unmarkNotPurchased(${item.id})">
|
||||
✅ Przywróć
|
||||
</button>
|
||||
` : `
|
||||
<button type="button" class="btn btn-outline-light"
|
||||
onclick="markNotPurchasedModal(event, ${item.id})">
|
||||
⚠️
|
||||
</button>
|
||||
${window.IS_SHARE ? `
|
||||
<button type="button" class="btn btn-outline-light"
|
||||
onclick="openNoteModal(event, ${item.id})">
|
||||
📝
|
||||
</button>
|
||||
` : ''}
|
||||
`}
|
||||
${!window.IS_SHARE ? `
|
||||
<button type="button" class="btn btn-outline-light"
|
||||
onclick="editItem(${item.id}, '${item.name.replace(/'/g, "\\'")}', ${item.quantity || 1})">
|
||||
✏️
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-light"
|
||||
onclick="deleteItem(${item.id})">
|
||||
🗑️
|
||||
</button>
|
||||
` : ''}
|
||||
</div>
|
||||
`;
|
||||
|
||||
const li = renderItem(item);
|
||||
fragment.appendChild(li);
|
||||
});
|
||||
|
||||
|
@@ -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 = `<span class="badge bg-secondary">x${data.quantity}</span>`;
|
||||
}
|
||||
|
||||
const countdownId = `countdown-${data.id}`;
|
||||
const countdownBtn = `
|
||||
<button type="button" class="btn btn-outline-warning" id="${countdownId}" disabled>15s</button>
|
||||
`;
|
||||
|
||||
li.innerHTML = `
|
||||
<div class="d-flex align-items-center flex-wrap gap-2 flex-grow-1">
|
||||
<input class="large-checkbox" type="checkbox">
|
||||
<span id="name-${data.id}" class="text-white">
|
||||
${data.name} ${quantityBadge}
|
||||
</span>
|
||||
</div>
|
||||
<div class="btn-group btn-group-sm" role="group">
|
||||
${countdownBtn}
|
||||
<button type="button" class="btn btn-outline-light"
|
||||
onclick="editItem(${data.id}, '${data.name.replace(/'/g, "\\'")}', ${data.quantity || 1})">
|
||||
✏️
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-light"
|
||||
onclick="deleteItem(${data.id})">
|
||||
🗑️
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
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) {
|
||||
|
103
static/js/user_expense_lists.js
Normal file
103
static/js/user_expense_lists.js
Normal file
@@ -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';
|
||||
});
|
||||
}
|
||||
});
|
@@ -131,11 +131,11 @@
|
||||
{% endif %}
|
||||
|
||||
{% if not is_share %}
|
||||
<button type="button" class="btn btn-outline-warning" {% if list.is_archived %}disabled{% else
|
||||
<button type="button" class="btn btn-outline-light" {% if list.is_archived %}disabled{% else
|
||||
%}onclick="editItem({{ item.id }}, '{{ item.name }}', {{ item.quantity or 1 }})" {% endif %}>
|
||||
✏️
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-danger" {% if list.is_archived %}disabled{% else
|
||||
<button type="button" class="btn btn-outline-light" {% if list.is_archived %}disabled{% else
|
||||
%}onclick="deleteItem({{ item.id }})" {% endif %}>
|
||||
🗑️
|
||||
</button>
|
||||
|
@@ -10,12 +10,14 @@
|
||||
<div class="card bg-dark text-white mb-5">
|
||||
<div class="card-body">
|
||||
<ul class="nav nav-tabs mb-3" id="expenseTabs" role="tablist">
|
||||
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link active" id="table-tab" data-bs-toggle="tab" data-bs-target="#tableTab" type="button"
|
||||
<button class="nav-link active" id="lists-tab" data-bs-toggle="tab" data-bs-target="#listsTab" type="button"
|
||||
role="tab">
|
||||
📄 Tabela
|
||||
📚 Listy
|
||||
</button>
|
||||
</li>
|
||||
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="chart-tab" data-bs-toggle="tab" data-bs-target="#chartTab" type="button"
|
||||
role="tab">
|
||||
@@ -25,32 +27,77 @@
|
||||
</ul>
|
||||
|
||||
<div class="tab-content" id="expenseTabsContent">
|
||||
<!-- Tabela -->
|
||||
<div class="tab-pane fade show active" id="tableTab" role="tabpanel">
|
||||
|
||||
<!-- 📚 LISTY -->
|
||||
<div class="tab-pane fade show active" id="listsTab" role="tabpanel">
|
||||
<div class="card bg-dark text-white mb-4">
|
||||
<div class="card-body">
|
||||
{% if expense_table %}
|
||||
<div class="row g-4">
|
||||
{% for row in expense_table %}
|
||||
<div class="col-12 col-sm-6 col-lg-4">
|
||||
<div class="card bg-dark text-white border border-secondary h-100 shadow-sm">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title text-truncate" title="{{ row.title }}">{{ row.title }}</h5>
|
||||
<p class="mb-1">💸 <strong>{{ '%.2f'|format(row.amount) }} PLN</strong></p>
|
||||
<p class="mb-0">📅 {{ row.added_at.strftime('%Y-%m-%d') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
<div class="d-flex flex-wrap gap-2 mb-3">
|
||||
<button class="btn btn-outline-light btn-sm filter-btn active" data-range="all">Wszystko</button>
|
||||
<button class="btn btn-outline-light btn-sm filter-btn" data-range="day">🗓️ Dzień</button>
|
||||
<button class="btn btn-outline-light btn-sm filter-btn" data-range="week">📆 Tydzień</button>
|
||||
<button class="btn btn-outline-light btn-sm filter-btn" data-range="month">📅 Miesiąc</button>
|
||||
<button class="btn btn-outline-light btn-sm filter-btn" data-range="year">📈 Rok</button>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="alert alert-info text-center mb-0">Brak wydatków do wyświetlenia.</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="row g-2 mb-3">
|
||||
<div class="col-6 col-md-3">
|
||||
<input type="date" id="customStart" class="form-control bg-dark text-white border-secondary rounded"
|
||||
placeholder="Data od">
|
||||
</div>
|
||||
<div class="col-6 col-md-3">
|
||||
<input type="date" id="customEnd" class="form-control bg-dark text-white border-secondary rounded"
|
||||
placeholder="Data do">
|
||||
</div>
|
||||
<div class="col-12 col-md-3">
|
||||
<button class="btn btn-outline-light w-100" id="applyCustomRange">📊 Zastosuj zakres</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-check mb-3">
|
||||
<input class="form-check-input" type="checkbox" id="onlyWithExpenses">
|
||||
<label class="form-check-label ms-2" for="onlyWithExpenses">Pokaż tylko listy z wydatkami</label>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-dark table-striped align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>Nazwa listy</th>
|
||||
<th>Data</th>
|
||||
<th>Wydatki (PLN)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="listsTableBody">
|
||||
{% for list in lists_data %}
|
||||
<tr data-date="{{ list.created_at.strftime('%Y-%m-%d') }}"
|
||||
data-week="{{ list.created_at.isocalendar()[0] }}-{{ '%02d' % list.created_at.isocalendar()[1] }}"
|
||||
data-month="{{ list.created_at.strftime('%Y-%m') }}" data-year="{{ list.created_at.year }}">
|
||||
<td>
|
||||
<input type="checkbox" class="form-check-input list-checkbox"
|
||||
data-amount="{{ '%.2f'|format(list.total_expense) }}">
|
||||
</td>
|
||||
<td>
|
||||
<strong>{{ list.title }}</strong>
|
||||
<br><small class="text-small">👤 {{ list.owner_username or '?' }}</small>
|
||||
|
||||
</td>
|
||||
<td>{{ list.created_at.strftime('%Y-%m-%d') }}</td>
|
||||
<td>{{ '%.2f'|format(list.total_expense) }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
<h5 class="text-success mt-3">💰 Suma zaznaczonych: <span id="listsTotal">0.00 PLN</span></h5>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Wykres -->
|
||||
|
||||
<!-- 📊 WYKRES -->
|
||||
<div class="tab-pane fade" id="chartTab" role="tabpanel">
|
||||
<div class="card bg-dark text-white mb-4">
|
||||
<div class="card-body">
|
||||
@@ -78,6 +125,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -86,4 +134,5 @@
|
||||
{% block scripts %}
|
||||
<script src="{{ url_for('static_bp.serve_js_lib', filename='chart.js') }}"></script>
|
||||
<script src="{{ url_for('static_bp.serve_js', filename='user_expenses.js') }}"></script>
|
||||
<script src="{{ url_for('static_bp.serve_js', filename='user_expense_lists.js') }}"></script>
|
||||
{% endblock %}
|
Reference in New Issue
Block a user