duzo zmian, funkcji
This commit is contained in:
@ -20,4 +20,10 @@ CREATE TABLE expense (
|
||||
added_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
receipt_filename VARCHAR(255),
|
||||
FOREIGN KEY(list_id) REFERENCES shopping_list(id)
|
||||
);
|
||||
);
|
||||
|
||||
# FUNKCJA UKRYCIA PUBLICZNIE LISTY
|
||||
ALTER TABLE shopping_list ADD COLUMN is_public BOOLEAN NOT NULL DEFAULT 1;
|
||||
|
||||
# ilośc produktów
|
||||
ALTER TABLE item ADD COLUMN quantity INTEGER DEFAULT 1;
|
||||
|
259
app.py
259
app.py
@ -49,7 +49,7 @@ class ShoppingList(db.Model):
|
||||
expires_at = db.Column(db.DateTime, nullable=True)
|
||||
owner = db.relationship('User', backref='lists', lazy=True)
|
||||
is_archived = db.Column(db.Boolean, default=False)
|
||||
|
||||
is_public = db.Column(db.Boolean, default=True)
|
||||
class Item(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
list_id = db.Column(db.Integer, db.ForeignKey('shopping_list.id'))
|
||||
@ -58,6 +58,7 @@ class Item(db.Model):
|
||||
added_by = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=True)
|
||||
purchased = db.Column(db.Boolean, default=False)
|
||||
purchased_at = db.Column(db.DateTime, nullable=True)
|
||||
quantity = db.Column(db.Integer, default=1)
|
||||
note = db.Column(db.Text, nullable=True)
|
||||
|
||||
class SuggestedProduct(db.Model):
|
||||
@ -166,23 +167,6 @@ def system_auth():
|
||||
flash('Nieprawidłowe hasło do systemu','danger')
|
||||
return render_template('system_auth.html')
|
||||
|
||||
@app.route('/')
|
||||
def index_guest():
|
||||
now = datetime.utcnow()
|
||||
lists = ShoppingList.query.filter(
|
||||
((ShoppingList.expires_at == None) | (ShoppingList.expires_at > now)),
|
||||
ShoppingList.is_archived == False
|
||||
).order_by(ShoppingList.created_at.desc()).all()
|
||||
|
||||
for l in lists:
|
||||
items = Item.query.filter_by(list_id=l.id).all()
|
||||
l.total_count = len(items)
|
||||
l.purchased_count = len([i for i in items if i.purchased])
|
||||
|
||||
expenses = Expense.query.filter_by(list_id=l.id).all()
|
||||
l.total_expense = sum(e.amount for e in expenses)
|
||||
|
||||
return render_template('index.html', lists=lists)
|
||||
|
||||
@app.errorhandler(404)
|
||||
def page_not_found(e):
|
||||
@ -197,6 +181,98 @@ def favicon():
|
||||
'''
|
||||
return svg, 200, {'Content-Type': 'image/svg+xml'}
|
||||
|
||||
@app.route('/')
|
||||
def index_guest():
|
||||
now = datetime.utcnow()
|
||||
|
||||
if current_user.is_authenticated:
|
||||
# Twoje listy
|
||||
user_lists = 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()
|
||||
|
||||
# Publiczne listy innych użytkowników
|
||||
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
|
||||
).order_by(ShoppingList.created_at.desc()).all()
|
||||
else:
|
||||
user_lists = []
|
||||
public_lists = 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()
|
||||
|
||||
# Liczenie produktów i wydatków
|
||||
for l in user_lists + public_lists:
|
||||
items = Item.query.filter_by(list_id=l.id).all()
|
||||
l.total_count = len(items)
|
||||
l.purchased_count = len([i for i in items if i.purchased])
|
||||
expenses = Expense.query.filter_by(list_id=l.id).all()
|
||||
l.total_expense = sum(e.amount for e in expenses)
|
||||
|
||||
return render_template("index.html", user_lists=user_lists, public_lists=public_lists)
|
||||
|
||||
@app.route('/archive_my_list/<int:list_id>')
|
||||
@login_required
|
||||
def archive_my_list(list_id):
|
||||
l = ShoppingList.query.get_or_404(list_id)
|
||||
if l.owner_id != current_user.id:
|
||||
flash('Nie masz uprawnień do tej listy', 'danger')
|
||||
return redirect(url_for('index_guest'))
|
||||
l.is_archived = True
|
||||
db.session.commit()
|
||||
flash('Lista została zarchiwizowana', 'success')
|
||||
return redirect(url_for('index_guest'))
|
||||
|
||||
@app.route('/edit_my_list/<int:list_id>', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def edit_my_list(list_id):
|
||||
l = ShoppingList.query.get_or_404(list_id)
|
||||
if l.owner_id != current_user.id:
|
||||
flash('Nie masz uprawnień do tej listy', 'danger')
|
||||
return redirect(url_for('index_guest'))
|
||||
|
||||
if request.method == 'POST':
|
||||
new_title = request.form.get('title')
|
||||
if new_title and new_title.strip():
|
||||
l.title = new_title.strip()
|
||||
db.session.commit()
|
||||
flash('Zaktualizowano tytuł listy', 'success')
|
||||
return redirect(url_for('index_guest'))
|
||||
else:
|
||||
flash('Podaj poprawny tytuł', 'danger')
|
||||
return render_template('edit_my_list.html', list=l)
|
||||
|
||||
@app.route('/toggle_visibility/<int:list_id>', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def toggle_visibility(list_id):
|
||||
l = ShoppingList.query.get_or_404(list_id)
|
||||
if l.owner_id != current_user.id:
|
||||
if request.is_json or request.method == 'POST':
|
||||
return {'error': 'Unauthorized'}, 403
|
||||
flash('Nie masz uprawnień do tej listy', 'danger')
|
||||
return redirect(url_for('index_guest'))
|
||||
|
||||
l.is_public = not l.is_public
|
||||
db.session.commit()
|
||||
|
||||
share_url = f"{request.url_root}share/{l.share_token}"
|
||||
|
||||
if request.is_json or request.method == 'POST':
|
||||
return {'is_public': l.is_public, 'share_url': share_url}
|
||||
|
||||
if l.is_public:
|
||||
flash('Lista została udostępniona publicznie', 'success')
|
||||
else:
|
||||
flash('Lista została ukryta przed gośćmi', 'info')
|
||||
|
||||
return redirect(url_for('index_guest'))
|
||||
|
||||
|
||||
@app.route('/login', methods=['GET', 'POST'])
|
||||
def login():
|
||||
if request.method == 'POST':
|
||||
@ -213,7 +289,7 @@ def login():
|
||||
def logout():
|
||||
logout_user()
|
||||
flash('Wylogowano pomyślnie', 'success')
|
||||
return redirect(url_for('login'))
|
||||
return redirect(url_for('index_guest'))
|
||||
|
||||
@app.route('/create', methods=['POST'])
|
||||
@login_required
|
||||
@ -257,6 +333,11 @@ def view_list(list_id):
|
||||
@app.route('/share/<token>')
|
||||
def share_list(token):
|
||||
shopping_list = ShoppingList.query.filter_by(share_token=token).first_or_404()
|
||||
|
||||
if not shopping_list.is_public:
|
||||
flash('Ta lista nie jest publicznie dostępna', 'danger')
|
||||
return redirect(url_for('index_guest'))
|
||||
|
||||
items = Item.query.filter_by(list_id=shopping_list.id).all()
|
||||
|
||||
receipt_pattern = f"list_{shopping_list.id}"
|
||||
@ -275,7 +356,6 @@ def share_list(token):
|
||||
total_expense=total_expense
|
||||
)
|
||||
|
||||
|
||||
@app.route('/guest-list/<int:list_id>')
|
||||
def guest_list(list_id):
|
||||
shopping_list = ShoppingList.query.get_or_404(list_id)
|
||||
@ -542,51 +622,6 @@ def delete_receipt(filename):
|
||||
flash('Plik nie istnieje', 'danger')
|
||||
return redirect(url_for('admin_receipts'))
|
||||
|
||||
@app.route('/admin/edit_list/<int:list_id>', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def edit_list(list_id):
|
||||
if not current_user.is_admin:
|
||||
return redirect(url_for('index_guest'))
|
||||
|
||||
l = ShoppingList.query.get_or_404(list_id)
|
||||
expenses = Expense.query.filter_by(list_id=list_id).all()
|
||||
total_expense = sum(e.amount for e in expenses)
|
||||
|
||||
if request.method == 'POST':
|
||||
new_title = request.form.get('title')
|
||||
new_amount_str = request.form.get('amount')
|
||||
is_archived = 'archived' in request.form
|
||||
|
||||
if new_title and new_title.strip():
|
||||
l.title = new_title.strip()
|
||||
|
||||
l.is_archived = is_archived
|
||||
|
||||
if new_amount_str:
|
||||
try:
|
||||
new_amount = float(new_amount_str)
|
||||
|
||||
|
||||
if expenses:
|
||||
for expense in expenses:
|
||||
db.session.delete(expense)
|
||||
db.session.commit()
|
||||
|
||||
new_expense = Expense(list_id=list_id, amount=new_amount)
|
||||
db.session.add(new_expense)
|
||||
db.session.commit()
|
||||
flash('Zaktualizowano tytuł, archiwizację i/lub kwotę wydatku', 'success')
|
||||
except ValueError:
|
||||
flash('Niepoprawna kwota', 'danger')
|
||||
return redirect(url_for('edit_list', list_id=list_id))
|
||||
else:
|
||||
db.session.commit()
|
||||
flash('Zaktualizowano tytuł i/lub archiwizację', 'success')
|
||||
|
||||
return redirect(url_for('admin_panel'))
|
||||
|
||||
return render_template('admin/edit_list.html', list=l, total_expense=total_expense)
|
||||
|
||||
|
||||
@app.route('/admin/delete_selected_lists', methods=['POST'])
|
||||
@login_required
|
||||
@ -625,6 +660,66 @@ def delete_all_items():
|
||||
return redirect(url_for('admin_panel'))
|
||||
|
||||
|
||||
@app.route('/admin/edit_list/<int:list_id>', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def edit_list(list_id):
|
||||
if not current_user.is_admin:
|
||||
return redirect(url_for('index_guest'))
|
||||
|
||||
l = ShoppingList.query.get_or_404(list_id)
|
||||
expenses = Expense.query.filter_by(list_id=list_id).all()
|
||||
total_expense = sum(e.amount for e in expenses)
|
||||
|
||||
users = User.query.all()
|
||||
|
||||
if request.method == 'POST':
|
||||
new_title = request.form.get('title')
|
||||
new_amount_str = request.form.get('amount')
|
||||
is_archived = 'archived' in request.form
|
||||
new_owner_id = request.form.get('owner_id')
|
||||
|
||||
if new_title and new_title.strip():
|
||||
l.title = new_title.strip()
|
||||
|
||||
l.is_archived = is_archived
|
||||
|
||||
if new_owner_id:
|
||||
try:
|
||||
new_owner_id_int = int(new_owner_id)
|
||||
if User.query.get(new_owner_id_int):
|
||||
l.owner_id = new_owner_id_int
|
||||
else:
|
||||
flash('Wybrany użytkownik nie istnieje', 'danger')
|
||||
return redirect(url_for('edit_list', list_id=list_id))
|
||||
except ValueError:
|
||||
flash('Niepoprawny ID użytkownika', 'danger')
|
||||
return redirect(url_for('edit_list', list_id=list_id))
|
||||
|
||||
if new_amount_str:
|
||||
try:
|
||||
new_amount = float(new_amount_str)
|
||||
|
||||
if expenses:
|
||||
for expense in expenses:
|
||||
db.session.delete(expense)
|
||||
db.session.commit()
|
||||
|
||||
new_expense = Expense(list_id=list_id, amount=new_amount)
|
||||
db.session.add(new_expense)
|
||||
db.session.commit()
|
||||
flash('Zaktualizowano tytuł, właściciela, archiwizację i/lub kwotę wydatku', 'success')
|
||||
except ValueError:
|
||||
flash('Niepoprawna kwota', 'danger')
|
||||
return redirect(url_for('edit_list', list_id=list_id))
|
||||
else:
|
||||
db.session.commit()
|
||||
flash('Zaktualizowano tytuł, właściciela i/lub archiwizację', 'success')
|
||||
|
||||
return redirect(url_for('admin_panel'))
|
||||
|
||||
return render_template('admin/edit_list.html', list=l, total_expense=total_expense, users=users)
|
||||
|
||||
|
||||
# =========================================================================================
|
||||
# SOCKET.IO
|
||||
# =========================================================================================
|
||||
@ -641,10 +736,28 @@ def handle_delete_item(data):
|
||||
def handle_edit_item(data):
|
||||
item = Item.query.get(data['item_id'])
|
||||
new_name = data['new_name']
|
||||
new_quantity = data.get('new_quantity', item.quantity)
|
||||
|
||||
if item and new_name.strip():
|
||||
item.name = new_name
|
||||
item.name = new_name.strip()
|
||||
|
||||
try:
|
||||
new_quantity = int(new_quantity)
|
||||
if new_quantity < 1:
|
||||
new_quantity = 1
|
||||
except:
|
||||
new_quantity = 1
|
||||
|
||||
item.quantity = new_quantity
|
||||
|
||||
db.session.commit()
|
||||
emit('item_edited', {'item_id': item.id, 'new_name': item.name}, to=str(item.list_id))
|
||||
|
||||
emit('item_edited', {
|
||||
'item_id': item.id,
|
||||
'new_name': item.name,
|
||||
'new_quantity': item.quantity
|
||||
}, to=str(item.list_id))
|
||||
|
||||
|
||||
@socketio.on('join_list')
|
||||
def handle_join(data):
|
||||
@ -657,9 +770,19 @@ def handle_join(data):
|
||||
def handle_add_item(data):
|
||||
list_id = data['list_id']
|
||||
name = data['name']
|
||||
quantity = data.get('quantity', 1)
|
||||
|
||||
try:
|
||||
quantity = int(quantity)
|
||||
if quantity < 1:
|
||||
quantity = 1
|
||||
except:
|
||||
quantity = 1
|
||||
|
||||
new_item = Item(
|
||||
list_id=list_id,
|
||||
name=name,
|
||||
quantity=quantity,
|
||||
added_by=current_user.id if current_user.is_authenticated else None
|
||||
)
|
||||
db.session.add(new_item)
|
||||
@ -669,9 +792,11 @@ def handle_add_item(data):
|
||||
db.session.add(new_suggestion)
|
||||
|
||||
db.session.commit()
|
||||
|
||||
emit('item_added', {
|
||||
'id': new_item.id,
|
||||
'name': new_item.name,
|
||||
'quantity': new_item.quantity,
|
||||
'added_by': current_user.username if current_user.is_authenticated else 'Gość'
|
||||
}, to=str(list_id), include_self=True)
|
||||
|
||||
|
29
static/js/hide_list.js
Normal file
29
static/js/hide_list.js
Normal file
@ -0,0 +1,29 @@
|
||||
function toggleVisibility(listId) {
|
||||
fetch('/toggle_visibility/' + listId, {method: 'POST'})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
const shareHeader = document.getElementById('share-header');
|
||||
const shareUrlSpan = document.getElementById('share-url');
|
||||
const copyBtn = document.getElementById('copyBtn');
|
||||
const toggleBtn = document.getElementById('toggleVisibilityBtn');
|
||||
|
||||
if (data.is_public) {
|
||||
shareHeader.textContent = '🔗 Udostępnij link:';
|
||||
shareUrlSpan.style.display = 'inline';
|
||||
shareUrlSpan.textContent = data.share_url;
|
||||
copyBtn.disabled = false;
|
||||
toggleBtn.innerHTML = '🙈 Ukryj listę';
|
||||
} else {
|
||||
shareHeader.textContent = '🙈 Lista jest ukryta przed gośćmi';
|
||||
shareUrlSpan.style.display = 'none';
|
||||
copyBtn.disabled = true;
|
||||
toggleBtn.innerHTML = '👁️ Udostępnij ponownie';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function copyLink(link) {
|
||||
navigator.clipboard.writeText(link).then(() => {
|
||||
alert('Link skopiowany!');
|
||||
});
|
||||
}
|
@ -93,6 +93,7 @@ function setupList(listId, username) {
|
||||
|
||||
socket.on('expense_added', data => {
|
||||
// Osobne bo w html musi byc unikatowy a wyswietlam w dwoch miejscach
|
||||
|
||||
const badgeEl = document.getElementById('total-expense1');
|
||||
if (badgeEl) {
|
||||
badgeEl.innerHTML = `💸 ${data.total.toFixed(2)} PLN`;
|
||||
@ -114,16 +115,23 @@ function setupList(listId, username) {
|
||||
const li = document.createElement('li');
|
||||
li.className = 'list-group-item d-flex justify-content-between align-items-center flex-wrap bg-light text-dark';
|
||||
li.id = `item-${data.id}`;
|
||||
|
||||
let quantityBadge = '';
|
||||
if (data.quantity && data.quantity > 1) {
|
||||
quantityBadge = `<span class="badge bg-secondary">x${data.quantity}</span>`;
|
||||
}
|
||||
|
||||
li.innerHTML = `
|
||||
<div class="d-flex align-items-center flex-wrap gap-2">
|
||||
<input type="checkbox">
|
||||
<span id="name-${data.id}" class="text-white">${data.name}</span>
|
||||
<span id="name-${data.id}" class="text-white">${data.name} ${quantityBadge}</span>
|
||||
</div>
|
||||
<div class="mt-2 mt-md-0">
|
||||
<button class="btn btn-sm btn-outline-warning me-1" onclick="editItem(${data.id}, '${data.name}')">✏️ Edytuj</button>
|
||||
<button class="btn btn-sm btn-outline-warning me-1" onclick="editItem(${data.id}, '${data.name}', ${data.quantity || 1})">✏️ Edytuj</button>
|
||||
<button class="btn btn-sm btn-outline-danger" onclick="deleteItem(${data.id})">🗑️ Usuń</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.getElementById('items').appendChild(li);
|
||||
updateProgressBar();
|
||||
});
|
||||
@ -181,9 +189,13 @@ function setupList(listId, username) {
|
||||
socket.on('item_edited', data => {
|
||||
const nameSpan = document.getElementById(`name-${data.item_id}`);
|
||||
if (nameSpan) {
|
||||
nameSpan.innerText = data.new_name;
|
||||
let quantityBadge = '';
|
||||
if (data.new_quantity && data.new_quantity > 1) {
|
||||
quantityBadge = ` <span class="badge bg-secondary">x${data.new_quantity}</span>`;
|
||||
}
|
||||
nameSpan.innerHTML = `${data.new_name}${quantityBadge}`;
|
||||
}
|
||||
showToast(`Zmieniono nazwę na: ${data.new_name}`);
|
||||
showToast(`Zaktualizowano produkt: ${data.new_name} (x${data.new_quantity})`);
|
||||
});
|
||||
|
||||
updateProgressBar();
|
||||
@ -225,23 +237,47 @@ function updateProgressBar() {
|
||||
|
||||
function addItem(listId) {
|
||||
const name = document.getElementById('newItem').value;
|
||||
const quantityInput = document.getElementById('newQuantity');
|
||||
let quantity = 1;
|
||||
|
||||
if (quantityInput) {
|
||||
quantity = parseInt(quantityInput.value);
|
||||
if (isNaN(quantity) || quantity < 1) {
|
||||
quantity = 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (name.trim() === '') return;
|
||||
socket.emit('add_item', { list_id: listId, name: name });
|
||||
|
||||
socket.emit('add_item', { list_id: listId, name: name, quantity: quantity });
|
||||
|
||||
document.getElementById('newItem').value = '';
|
||||
if (quantityInput) quantityInput.value = 1;
|
||||
document.getElementById('newItem').focus();
|
||||
}
|
||||
|
||||
|
||||
function deleteItem(id) {
|
||||
if (confirm('Na pewno usunąć produkt?')) {
|
||||
socket.emit('delete_item', { item_id: id });
|
||||
}
|
||||
}
|
||||
|
||||
function editItem(id, oldName) {
|
||||
const newName = prompt('Podaj nową nazwę:', oldName);
|
||||
if (newName && newName.trim() !== '') {
|
||||
socket.emit('edit_item', { item_id: id, new_name: newName });
|
||||
function editItem(id, oldName, oldQuantity) {
|
||||
const newName = prompt('Podaj nową nazwę (lub zostaw starą):', oldName);
|
||||
if (newName === null) return; // anulował
|
||||
|
||||
const newQuantityStr = prompt('Podaj nową ilość:', oldQuantity);
|
||||
if (newQuantityStr === null) return; // anulował
|
||||
|
||||
const finalName = newName.trim() !== '' ? newName.trim() : oldName;
|
||||
|
||||
let newQuantity = parseInt(newQuantityStr);
|
||||
if (isNaN(newQuantity) || newQuantity < 1) {
|
||||
newQuantity = oldQuantity;
|
||||
}
|
||||
|
||||
socket.emit('edit_item', { item_id: id, new_name: finalName, new_quantity: newQuantity });
|
||||
}
|
||||
|
||||
function submitExpense() {
|
||||
|
@ -92,6 +92,7 @@
|
||||
<th><input type="checkbox" id="select-all"></th>
|
||||
<th>ID</th>
|
||||
<th>Tytuł</th>
|
||||
<th>Status</th>
|
||||
<th>Utworzono</th>
|
||||
<th>Właściciel</th>
|
||||
<th>Produkty</th>
|
||||
@ -111,6 +112,15 @@
|
||||
<td class="fw-bold">
|
||||
<a href="{{ url_for('view_list', list_id=l.id) }}" class="text-white">{{ l.title }}</a>
|
||||
</td>
|
||||
<td>
|
||||
{% if l.is_archived %}
|
||||
<span class="badge bg-secondary">Archiwalna</span>
|
||||
{% elif l.is_temporary and l.expires_at and l.expires_at < now %}
|
||||
<span class="badge bg-warning text-dark">Wygasła</span>
|
||||
{% else %}
|
||||
<span class="badge bg-success">Aktywna</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ l.created_at.strftime('%Y-%m-%d %H:%M') if l.created_at else '-' }}</td>
|
||||
<td>
|
||||
{% if l.owner_id %}
|
||||
|
@ -19,6 +19,17 @@
|
||||
<small class="form-text text-muted">Jeśli nie chcesz zmieniać kwoty, zostaw to pole bez zmian.</small>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="owner_id" class="form-label">Nowy właściciel</label>
|
||||
<select class="form-select" id="owner_id" name="owner_id">
|
||||
{% for user in users %}
|
||||
<option value="{{ user.id }}" {% if list.owner_id == user.id %}selected{% endif %}>
|
||||
{{ user.username }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-check mb-3">
|
||||
<input class="form-check-input" type="checkbox" id="archived" name="archived" {% if list.is_archived %}checked{% endif %}>
|
||||
<label class="form-check-label" for="archived">
|
||||
|
@ -11,6 +11,7 @@
|
||||
<script src="https://cdn.socket.io/4.6.1/socket.io.min.js"></script>
|
||||
<script src="{{ url_for('static_bp.serve_live_js') }}?v={{ time.time() | int }}"></script>
|
||||
<script src="{{ url_for('static', filename='js/toasts.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='js/hide_list.js') }}"></script>
|
||||
</head>
|
||||
<body class="bg-dark text-white">
|
||||
|
||||
|
12
templates/edit_my_list.html
Normal file
12
templates/edit_my_list.html
Normal file
@ -0,0 +1,12 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block content %}
|
||||
<h2>Edytuj listę: {{ list.title }}</h2>
|
||||
<form method="post">
|
||||
<div class="mb-3">
|
||||
<label for="title" class="form-label">Nowy tytuł</label>
|
||||
<input type="text" name="title" id="title" class="form-control" value="{{ list.title }}" required>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-success">Zapisz</button>
|
||||
<a href="{{ url_for('index_guest') }}" class="btn btn-secondary">Anuluj</a>
|
||||
</form>
|
||||
{% endblock %}
|
@ -2,60 +2,97 @@
|
||||
{% block title %}Twoje listy zakupów{% endblock %}
|
||||
{% block content %}
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center flex-wrap mb-4">
|
||||
<h2 class="mb-2">Stwórz nową listę</h2>
|
||||
</div>
|
||||
|
||||
<div class="card bg-dark text-white mb-4">
|
||||
<div class="card-body">
|
||||
<form action="/create" method="post">
|
||||
<div class="input-group mb-3">
|
||||
<input type="text" name="title" id="title" placeholder="Wprowadź nazwę nowej listy" required class="form-control">
|
||||
<div class="input-group-text">
|
||||
<input type="checkbox" name="temporary" class="form-check-input m-0" id="tempCheck">
|
||||
<label for="tempCheck" class="ms-2 mb-0">Tymczasowa (7 dni)</label>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-success w-100">➕ Utwórz nową listę</button>
|
||||
</form>
|
||||
{% if not current_user.is_authenticated %}
|
||||
<div class="alert alert-info" role="alert">
|
||||
Jesteś w trybie gościa. Możesz tylko przeglądać listy udostępnione publicznie.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center flex-wrap mb-4">
|
||||
<h2 class="mb-2">Listy zakupów</h2>
|
||||
</div>
|
||||
|
||||
{% if lists %}
|
||||
<ul class="list-group">
|
||||
{% for l in lists %}
|
||||
{% set purchased_count = l.purchased_count if l.purchased_count is defined else 0 %}
|
||||
{% set total_count = l.total_count if l.total_count is defined else 0 %}
|
||||
{% set percent = (purchased_count / total_count * 100) if total_count > 0 else 0 %}
|
||||
<li class="list-group-item bg-dark text-white">
|
||||
<div class="d-flex justify-content-between align-items-center flex-wrap w-100">
|
||||
<span class="fw-bold">{{ l.title }} (Autor: {{ l.owner.username }})</span>
|
||||
<div class="mt-2 mt-md-0">
|
||||
{% if current_user.is_authenticated %}
|
||||
<a href="/list/{{ l.id }}" class="btn btn-sm btn-outline-light me-1">📄 Otwórz</a>
|
||||
<a href="/copy/{{ l.id }}" class="btn btn-sm btn-outline-secondary">📋 Kopiuj</a>
|
||||
{% else %}
|
||||
<a href="/guest-list/{{ l.id }}" class="btn btn-sm btn-outline-light me-1">📄 Otwórz</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="progress mt-2" style="height: 20px;">
|
||||
<div class="progress-bar bg-warning text-dark fw-bold" role="progressbar" style="width: {{ percent }}%" aria-valuenow="{{ percent }}" aria-valuemin="0" aria-valuemax="100">
|
||||
Produkty: {{ purchased_count }}/{{ total_count }} ({{ percent|round(0) }}%)
|
||||
{% if l.total_expense > 0 %}
|
||||
— 💸 {{ '%.2f'|format(l.total_expense) }} PLN
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<p><span class="badge bg-secondary">Nie masz jeszcze żadnych list. Utwórz pierwszą, korzystając z formularza powyżej!</span></p>
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
{% if current_user.is_authenticated %}
|
||||
<div class="d-flex justify-content-between align-items-center flex-wrap mb-4">
|
||||
<h2 class="mb-2">Stwórz nową listę</h2>
|
||||
</div>
|
||||
|
||||
<div class="card bg-dark text-white mb-4">
|
||||
<div class="card-body">
|
||||
<form action="/create" method="post">
|
||||
<div class="input-group mb-3">
|
||||
<input type="text" name="title" id="title" placeholder="Wprowadź nazwę nowej listy" required class="form-control">
|
||||
<div class="input-group-text">
|
||||
<input type="checkbox" name="temporary" class="form-check-input m-0" id="tempCheck">
|
||||
<label for="tempCheck" class="ms-2 mb-0">Tymczasowa (7 dni)</label>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-success w-100">➕ Utwórz nową listę</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if current_user.is_authenticated %}
|
||||
<h3 class="mt-4">Twoje listy</h3>
|
||||
{% if user_lists %}
|
||||
<ul class="list-group mb-4">
|
||||
{% for l in user_lists %}
|
||||
{% set purchased_count = l.purchased_count %}
|
||||
{% set total_count = l.total_count %}
|
||||
{% set percent = (purchased_count / total_count * 100) if total_count > 0 else 0 %}
|
||||
<li class="list-group-item bg-dark text-white">
|
||||
<div class="d-flex justify-content-between align-items-center flex-wrap w-100">
|
||||
<span class="fw-bold">{{ l.title }} (Autor: Ty)</span>
|
||||
<div class="mt-2 mt-md-0">
|
||||
<a href="/list/{{ l.id }}" class="btn btn-sm btn-outline-light me-1">📄 Otwórz</a>
|
||||
<a href="/copy/{{ l.id }}" class="btn btn-sm btn-outline-secondary me-1">📋 Kopiuj</a>
|
||||
<a href="/edit_my_list/{{ l.id }}" class="btn btn-sm btn-outline-warning me-1">✏️ Edytuj</a>
|
||||
<a href="/archive_my_list/{{ l.id }}" class="btn btn-sm btn-outline-danger me-1">🗄️ Archiwizuj</a>
|
||||
{% if l.is_public %}
|
||||
<a href="/toggle_visibility/{{ l.id }}" class="btn btn-sm btn-outline-secondary">🙈 Ukryj</a>
|
||||
{% else %}
|
||||
<a href="/toggle_visibility/{{ l.id }}" class="btn btn-sm btn-outline-success">👁️ Udostępnij</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="progress mt-2" style="height: 20px;">
|
||||
<div class="progress-bar bg-warning text-dark fw-bold" role="progressbar" style="width: {{ percent }}%" aria-valuenow="{{ percent }}" aria-valuemin="0" aria-valuemax="100">
|
||||
Produkty: {{ purchased_count }}/{{ total_count }} ({{ percent|round(0) }}%)
|
||||
{% if l.total_expense > 0 %}
|
||||
— 💸 {{ '%.2f'|format(l.total_expense) }} PLN
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<p><span class="badge bg-secondary">Nie masz jeszcze żadnych list. Utwórz pierwszą, korzystając z formularza powyżej!</span></p>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
<h3 class="mt-4">Publiczne listy innych użytkowników</h3>
|
||||
{% if public_lists %}
|
||||
<ul class="list-group">
|
||||
{% for l in public_lists %}
|
||||
{% set purchased_count = l.purchased_count %}
|
||||
{% set total_count = l.total_count %}
|
||||
{% set percent = (purchased_count / total_count * 100) if total_count > 0 else 0 %}
|
||||
<li class="list-group-item bg-dark text-white">
|
||||
<div class="d-flex justify-content-between align-items-center flex-wrap w-100">
|
||||
<span class="fw-bold">{{ l.title }} (Autor: {{ l.owner.username }})</span>
|
||||
<a href="/guest-list/{{ l.id }}" class="btn btn-sm btn-outline-light">📄 Otwórz</a>
|
||||
</div>
|
||||
<div class="progress mt-2" style="height: 20px;">
|
||||
<div class="progress-bar bg-warning text-dark fw-bold" role="progressbar" style="width: {{ percent }}%" aria-valuenow="{{ percent }}" aria-valuemin="0" aria-valuemax="100">
|
||||
Produkty: {{ purchased_count }}/{{ total_count }} ({{ percent|round(0) }}%)
|
||||
{% if l.total_expense > 0 %}
|
||||
— 💸 {{ '%.2f'|format(l.total_expense) }} PLN
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<p><span class="badge bg-secondary">Brak dostępnych list publicznych do wyświetlenia.</span></p>
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
|
@ -1,5 +1,5 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block title %}{{ list.title }}{% endblock %}
|
||||
{% block title %}Lista: {{ list.title }}{% endblock %}
|
||||
{% block content %}
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mb-3 flex-wrap">
|
||||
@ -11,28 +11,37 @@
|
||||
{% endif %}
|
||||
</h2>
|
||||
|
||||
|
||||
|
||||
<a href="/" class="btn btn-outline-secondary">← Powrót do list</a>
|
||||
</div>
|
||||
|
||||
<div class="card bg-dark text-white mb-4">
|
||||
<div id="share-card" class="card bg-dark text-white mb-4">
|
||||
<div class="card-body d-flex flex-column flex-md-row justify-content-between align-items-start align-items-md-center gap-2">
|
||||
<div>
|
||||
<strong>🔗 Udostępnij link:</strong><br>
|
||||
<span class="badge bg-secondary text-wrap" style="font-size: 0.7rem;">
|
||||
<strong id="share-header">
|
||||
{% if list.is_public %}
|
||||
🔗 Udostępnij link:
|
||||
{% else %}
|
||||
🙈 Lista jest ukryta przed gośćmi
|
||||
{% endif %}
|
||||
</strong><br>
|
||||
<span id="share-url" class="badge bg-secondary text-wrap" style="font-size: 0.7rem; {% if not list.is_public %}display: none;{% endif %}">
|
||||
{{ request.url_root }}share/{{ list.share_token }}
|
||||
</span>
|
||||
</div>
|
||||
{% if not list.is_archived %}
|
||||
<button class="btn btn-success btn-sm mt-2 mt-md-0" onclick="copyLink('{{ request.url_root }}share/{{ list.share_token }}')">
|
||||
📋 Udostępnij link
|
||||
<div class="d-flex flex-column flex-md-row gap-2 mt-2 mt-md-0">
|
||||
<button id="copyBtn" class="btn btn-success btn-sm"
|
||||
onclick="copyLink('{{ request.url_root }}share/{{ list.share_token }}')"
|
||||
{% if not list.is_public %}disabled{% endif %}>
|
||||
📋 Skopiuj link
|
||||
</button>
|
||||
{% else %}
|
||||
<button class="btn btn-secondary btn-sm mt-2 mt-md-0" disabled>
|
||||
📋 Udostępnianie wyłączone (Archiwalna)
|
||||
<button id="toggleVisibilityBtn" class="btn btn-outline-secondary btn-sm" onclick="toggleVisibility({{ list.id }})">
|
||||
{% if list.is_public %}
|
||||
🙈 Ukryj listę
|
||||
{% else %}
|
||||
👁️ Udostępnij ponownie
|
||||
{% endif %}
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -65,32 +74,37 @@
|
||||
<li class="list-group-item d-flex justify-content-between align-items-center flex-wrap {% if item.purchased %}bg-success text-white{% else %}bg-light text-white{% endif %}" id="item-{{ item.id }}">
|
||||
<div class="d-flex align-items-center flex-wrap gap-2 flex-grow-1">
|
||||
<input type="checkbox" {% if item.purchased %}checked{% endif %} {% if list.is_archived %}disabled{% endif %}>
|
||||
<span id="name-{{ item.id }}" class="{% if item.purchased %}text-white{% else %}text-white{% endif %}">{{ item.name }}</span>
|
||||
<span id="name-{{ item.id }}" class="{% if item.purchased %}text-white{% else %}text-white{% endif %}">
|
||||
{{ item.name }}
|
||||
{% if item.quantity and item.quantity > 1 %}
|
||||
<span class="badge bg-secondary">x{{ item.quantity }}</span>
|
||||
{% endif %}
|
||||
</span>
|
||||
{% if item.note %}
|
||||
<small class="text-danger">[ Notatka: <b>{{ item.note }}</b> ] </small>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="mt-2 mt-md-0 d-flex gap-1">
|
||||
<button class="btn btn-sm btn-outline-warning"
|
||||
{% if list.is_archived %}disabled{% else %}onclick="editItem({{ item.id }}, '{{ item.name }}')"{% endif %}>
|
||||
{% if list.is_archived %}disabled{% else %}onclick="editItem({{ item.id }}, '{{ item.name }}', {{ item.quantity or 1 }})"{% endif %}>
|
||||
✏️ Edytuj
|
||||
</button>
|
||||
|
||||
<button class="btn btn-sm btn-outline-danger"
|
||||
{% if list.is_archived %}disabled{% else %}onclick="deleteItem({{ item.id }})"{% endif %}>
|
||||
🗑️ Usuń
|
||||
</button>
|
||||
|
||||
</div>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
{% if not list.is_archived %}
|
||||
<div class="input-group mb-2 position-relative">
|
||||
<input id="newItem" class="form-control" placeholder="Nowy produkt">
|
||||
<button onclick="addItem({{ list.id }})" class="btn btn-success">➕ Dodaj</button>
|
||||
</div>
|
||||
<div class="input-group mb-3">
|
||||
<input type="text" id="newItem" name="name" class="form-control" placeholder="Nowy produkt" required>
|
||||
<input type="number" id="newQuantity" name="quantity" class="form-control" placeholder="Ilość" min="1" value="1" style="max-width: 80px;">
|
||||
<button type="button" class="btn btn-success" onclick="addItem({{ list.id }})">➕ Dodaj</button>
|
||||
</div>
|
||||
|
||||
{% endif %}
|
||||
|
||||
{% set receipt_pattern = 'list_' ~ list.id %}
|
||||
|
@ -23,7 +23,13 @@
|
||||
<li class="list-group-item d-flex justify-content-between align-items-center flex-wrap clickable-item {% if item.purchased %}bg-success text-white{% else %}bg-light text-white{% endif %}" id="item-{{ item.id }}">
|
||||
<div class="d-flex align-items-center gap-3 flex-grow-1">
|
||||
<input type="checkbox" class="form-check-input large-checkbox" {% if item.purchased %}checked{% endif %} {% if list.is_archived %}disabled{% endif %}>
|
||||
<span id="name-{{ item.id }}" class="{% if item.purchased %}text-white{% else %}text-white{% endif %}">{{ item.name }}</span>
|
||||
<span id="name-{{ item.id }}" class="{% if item.purchased %}text-white{% else %}text-white{% endif %}">
|
||||
{{ item.name }}
|
||||
{% if item.quantity and item.quantity > 1 %}
|
||||
<span class="badge bg-secondary">x{{ item.quantity }}</span>
|
||||
{% endif %}
|
||||
</span>
|
||||
|
||||
{% if item.note %}
|
||||
<small class="text-danger ms-4">[ Notatka: <b>{{ item.note }}</b> ]</small>
|
||||
{% endif %}
|
||||
@ -39,6 +45,7 @@
|
||||
{% if not list.is_archived %}
|
||||
<div class="input-group mb-2">
|
||||
<input id="newItem" class="form-control" placeholder="Nowy produkt">
|
||||
<input id="newQuantity" type="number" class="form-control" placeholder="Ilość" min="1" value="1" style="max-width: 90px;">
|
||||
<button onclick="addItem({{ list.id }})" class="btn btn-success">➕ Dodaj</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
Reference in New Issue
Block a user