poprawkiA
This commit is contained in:
17
app.py
17
app.py
@ -3,7 +3,7 @@ import secrets
|
||||
import time
|
||||
import mimetypes
|
||||
from datetime import datetime, timedelta
|
||||
from flask import Flask, render_template, redirect, url_for, request, flash, Blueprint, send_from_directory, request, abort
|
||||
from flask import Flask, render_template, redirect, url_for, request, flash, Blueprint, send_from_directory, request, abort, session
|
||||
from markupsafe import Markup
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
from flask_login import LoginManager, UserMixin, login_user, login_required, logout_user, current_user
|
||||
@ -832,18 +832,31 @@ def handle_edit_item(data):
|
||||
'new_quantity': item.quantity
|
||||
}, to=str(item.list_id))
|
||||
|
||||
|
||||
@socketio.on('join_list')
|
||||
def handle_join(data):
|
||||
room = str(data['room'])
|
||||
username = data.get('username', 'Gość')
|
||||
join_room(room)
|
||||
|
||||
if room not in active_users:
|
||||
active_users[room] = set()
|
||||
active_users[room].add(username)
|
||||
|
||||
shopping_list = ShoppingList.query.get(int(data['room']))
|
||||
list_title = shopping_list.title if shopping_list else "Twoja lista"
|
||||
|
||||
emit('user_joined', {'username': username}, to=room)
|
||||
emit('user_list', {'users': list(active_users[room])}, to=room)
|
||||
emit('joined_confirmation', {'room': room, 'list_title': list_title})
|
||||
|
||||
@socketio.on('disconnect')
|
||||
def handle_disconnect():
|
||||
for room, users in active_users.items():
|
||||
if current_user.username in users:
|
||||
users.remove(current_user.username)
|
||||
emit('user_left', {'username': current_user.username}, to=room)
|
||||
emit('user_list', {'users': list(users)}, to=room)
|
||||
|
||||
@socketio.on('add_item')
|
||||
def handle_add_item(data):
|
||||
list_id = data['list_id']
|
||||
|
@ -196,6 +196,20 @@ function setupList(listId, username) {
|
||||
window.LIST_ID = listId;
|
||||
window.usernameForReconnect = username;
|
||||
|
||||
|
||||
socket.on('user_joined', function(data) {
|
||||
showToast(`${data.username} dołączył do listy 👥`, 'info');
|
||||
});
|
||||
|
||||
socket.on('user_left', function(data) {
|
||||
showToast(`${data.username} opuścił listę ❌`, 'warning');
|
||||
});
|
||||
|
||||
socket.on('user_list', function(data) {
|
||||
const userList = data.users.join(', ');
|
||||
showToast(`Obecni: ${userList}`, 'info');
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
function updateItemState(itemId, isChecked) {
|
||||
|
@ -1,319 +0,0 @@
|
||||
const socket = io();
|
||||
|
||||
function setupList(listId, username) {
|
||||
socket.emit('join_list', { room: listId, username: username });
|
||||
|
||||
const newItemInput = document.getElementById('newItem');
|
||||
|
||||
const parentDiv = newItemInput.closest('.input-group');
|
||||
if (parentDiv) {
|
||||
parentDiv.classList.add('position-relative');
|
||||
}
|
||||
|
||||
const suggestionsBox = document.createElement('div');
|
||||
suggestionsBox.className = 'list-group position-absolute w-100';
|
||||
suggestionsBox.style.top = '100%';
|
||||
suggestionsBox.style.left = '0';
|
||||
suggestionsBox.style.zIndex = '9999';
|
||||
newItemInput.parentNode.appendChild(suggestionsBox);
|
||||
|
||||
newItemInput.addEventListener('input', async function () {
|
||||
const query = this.value;
|
||||
if (query.length < 2) {
|
||||
suggestionsBox.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await fetch(`/suggest_products?q=${encodeURIComponent(query)}`);
|
||||
const data = await res.json();
|
||||
|
||||
suggestionsBox.innerHTML = '';
|
||||
data.suggestions.forEach(s => {
|
||||
const item = document.createElement('button');
|
||||
item.type = 'button';
|
||||
item.className = 'list-group-item list-group-item-action';
|
||||
item.textContent = s;
|
||||
item.onclick = () => {
|
||||
newItemInput.value = s;
|
||||
suggestionsBox.innerHTML = '';
|
||||
};
|
||||
suggestionsBox.appendChild(item);
|
||||
});
|
||||
});
|
||||
|
||||
newItemInput.addEventListener('blur', () => {
|
||||
setTimeout(() => { suggestionsBox.innerHTML = ''; }, 200);
|
||||
});
|
||||
|
||||
newItemInput.focus();
|
||||
newItemInput.addEventListener('keypress', function (e) {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
addItem(listId);
|
||||
}
|
||||
});
|
||||
|
||||
const itemsContainer = document.getElementById('items');
|
||||
|
||||
itemsContainer.addEventListener('change', function (e) {
|
||||
if (e.target && e.target.type === 'checkbox') {
|
||||
const li = e.target.closest('li');
|
||||
if (li) {
|
||||
const id = parseInt(li.id.replace('item-', ''), 10);
|
||||
|
||||
if (e.target.checked) {
|
||||
socket.emit('check_item', { item_id: id });
|
||||
} else {
|
||||
socket.emit('uncheck_item', { item_id: id });
|
||||
}
|
||||
|
||||
e.target.disabled = true;
|
||||
li.classList.add('opacity-50');
|
||||
|
||||
let existingSpinner = li.querySelector('.spinner-border');
|
||||
if (!existingSpinner) {
|
||||
const spinner = document.createElement('span');
|
||||
spinner.className = 'spinner-border spinner-border-sm ms-2';
|
||||
spinner.setAttribute('role', 'status');
|
||||
spinner.setAttribute('aria-hidden', 'true');
|
||||
e.target.parentElement.appendChild(spinner);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('item_checked', data => {
|
||||
updateItemState(data.item_id, true);
|
||||
});
|
||||
|
||||
socket.on('item_unchecked', data => {
|
||||
updateItemState(data.item_id, false);
|
||||
});
|
||||
|
||||
socket.on('expense_added', data => {
|
||||
const badgeEl = document.getElementById('total-expense1');
|
||||
if (badgeEl) {
|
||||
badgeEl.innerHTML = `💸 ${data.total.toFixed(2)} PLN`;
|
||||
badgeEl.classList.remove('bg-secondary');
|
||||
badgeEl.classList.add('bg-success');
|
||||
badgeEl.style.display = '';
|
||||
}
|
||||
|
||||
const summaryEl = document.getElementById('total-expense2');
|
||||
if (summaryEl) {
|
||||
summaryEl.innerHTML = `<b>💸 Łącznie wydano:</b> ${data.total.toFixed(2)} PLN`;
|
||||
}
|
||||
|
||||
showToast(`Dodano wydatek: ${data.amount.toFixed(2)} PLN`);
|
||||
});
|
||||
|
||||
socket.on('item_added', data => {
|
||||
showToast(`${data.added_by} dodał: ${data.name}`);
|
||||
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>`;
|
||||
}
|
||||
|
||||
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} ${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}', ${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();
|
||||
});
|
||||
|
||||
socket.on('item_deleted', data => {
|
||||
const li = document.getElementById(`item-${data.item_id}`);
|
||||
if (li) {
|
||||
li.remove();
|
||||
}
|
||||
showToast('Usunięto produkt');
|
||||
updateProgressBar();
|
||||
});
|
||||
|
||||
socket.on('progress_updated', function(data) {
|
||||
const progressBar = document.getElementById('progress-bar');
|
||||
if (progressBar) {
|
||||
progressBar.style.width = data.percent + '%';
|
||||
progressBar.setAttribute('aria-valuenow', data.percent);
|
||||
progressBar.textContent = `${Math.round(data.percent)}%`;
|
||||
}
|
||||
|
||||
const progressTitle = document.getElementById('progress-title');
|
||||
if (progressTitle) {
|
||||
progressTitle.textContent = `📊 Postęp listy — ${data.purchased_count}/${data.total_count} kupionych (${Math.round(data.percent)}%)`;
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('note_updated', data => {
|
||||
const itemEl = document.getElementById(`item-${data.item_id}`);
|
||||
if (itemEl) {
|
||||
let noteEl = itemEl.querySelector('small');
|
||||
if (noteEl) {
|
||||
noteEl.innerHTML = `[ Notatka: <b>${data.note}</b> ]`;
|
||||
} else {
|
||||
const newNote = document.createElement('small');
|
||||
newNote.className = 'text-danger ms-4';
|
||||
newNote.innerHTML = `[ Notatka: <b>${data.note}</b> ]`;
|
||||
|
||||
const flexColumn = itemEl.querySelector('.d-flex.flex-column');
|
||||
if (flexColumn) {
|
||||
flexColumn.appendChild(newNote);
|
||||
} else {
|
||||
itemEl.appendChild(newNote);
|
||||
}
|
||||
}
|
||||
}
|
||||
showToast('Notatka zaktualizowana!');
|
||||
});
|
||||
|
||||
socket.on('item_edited', data => {
|
||||
const nameSpan = document.getElementById(`name-${data.item_id}`);
|
||||
if (nameSpan) {
|
||||
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(`Zaktualizowano produkt: ${data.new_name} (x${data.new_quantity})`);
|
||||
});
|
||||
|
||||
updateProgressBar();
|
||||
|
||||
// --- NOWE: zapisz dane do reconnect ---
|
||||
window.LIST_ID = listId;
|
||||
window.usernameForReconnect = username;
|
||||
|
||||
socket.on('joined_confirmation', function(data) {
|
||||
console.log('Dołączono do pokoju:', data.room);
|
||||
showToast('Pokój ponownie dołączony ✅', 'info');
|
||||
});
|
||||
}
|
||||
|
||||
function updateItemState(itemId, isChecked) {
|
||||
const checkbox = document.querySelector(`#item-${itemId} input[type='checkbox']`);
|
||||
if (checkbox) {
|
||||
checkbox.checked = isChecked;
|
||||
checkbox.disabled = false;
|
||||
const li = checkbox.closest('li');
|
||||
li.classList.remove('opacity-50', 'bg-light', 'text-dark', 'bg-success', 'text-white');
|
||||
|
||||
if (isChecked) {
|
||||
li.classList.add('bg-success', 'text-white');
|
||||
} else {
|
||||
li.classList.add('item-not-checked');
|
||||
}
|
||||
|
||||
const sp = li.querySelector('.spinner-border');
|
||||
if (sp) sp.remove();
|
||||
}
|
||||
updateProgressBar();
|
||||
}
|
||||
|
||||
function updateProgressBar() {
|
||||
const items = document.querySelectorAll('#items li');
|
||||
const total = items.length;
|
||||
const purchased = Array.from(items).filter(li => li.classList.contains('bg-success')).length;
|
||||
const percent = total > 0 ? Math.round((purchased / total) * 100) : 0;
|
||||
|
||||
const progressBar = document.getElementById('progress-bar');
|
||||
if (progressBar) {
|
||||
progressBar.style.width = `${percent}%`;
|
||||
progressBar.setAttribute('aria-valuenow', percent);
|
||||
progressBar.textContent = `${percent}%`;
|
||||
}
|
||||
}
|
||||
|
||||
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, 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, oldQuantity) {
|
||||
const newName = prompt('Podaj nową nazwę (lub zostaw starą):', oldName);
|
||||
if (newName === null) return;
|
||||
|
||||
const newQuantityStr = prompt('Podaj nową ilość:', oldQuantity);
|
||||
if (newQuantityStr === null) return;
|
||||
|
||||
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(listId) {
|
||||
const amountInput = document.getElementById('expenseAmount');
|
||||
const amount = parseFloat(amountInput.value);
|
||||
if (isNaN(amount) || amount <= 0) {
|
||||
showToast('Podaj poprawną kwotę!');
|
||||
return;
|
||||
}
|
||||
socket.emit('add_expense', {
|
||||
list_id: listId,
|
||||
amount: amount
|
||||
});
|
||||
amountInput.value = '';
|
||||
}
|
||||
|
||||
function copyLink(link) {
|
||||
if (navigator.share) {
|
||||
navigator.share({
|
||||
title: 'Lista zakupów',
|
||||
text: 'Udostępniam Ci moją listę zakupów:',
|
||||
url: link
|
||||
}).catch((err) => console.log('Udostępnianie anulowane', err));
|
||||
} else {
|
||||
navigator.clipboard.writeText(link).then(() => {
|
||||
showToast('Link skopiowany do schowka!');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function showToast(message, type = 'primary') {
|
||||
const toastContainer = document.getElementById('toast-container');
|
||||
const toast = document.createElement('div');
|
||||
toast.className = `toast align-items-center text-bg-${type} border-0 show`;
|
||||
toast.setAttribute('role', 'alert');
|
||||
toast.innerHTML = `<div class="d-flex"><div class="toast-body">${message}</div></div>`;
|
||||
toastContainer.appendChild(toast);
|
||||
setTimeout(() => { toast.remove(); }, 1750);
|
||||
}
|
@ -43,6 +43,11 @@ socket.on('connect', function() {
|
||||
firstConnect = false;
|
||||
});
|
||||
|
||||
socket.on('disconnect', function(reason) {
|
||||
showToast('Utracono połączenie z serwerem...', 'warning');
|
||||
disableCheckboxes(true);
|
||||
});
|
||||
|
||||
socket.off('joined_confirmation');
|
||||
socket.on('joined_confirmation', function(data) {
|
||||
if (wasReconnected) {
|
||||
|
Reference in New Issue
Block a user