ux i funkcja masowego dodwania produktu
This commit is contained in:
@@ -27,3 +27,7 @@ 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;
|
||||
|
||||
#licznik najczesciej kupowanych reczy
|
||||
ALTER TABLE suggested_product ADD COLUMN usage_count INTEGER DEFAULT 0;
|
||||
|
||||
|
41
app.py
41
app.py
@@ -98,7 +98,7 @@ class Item(db.Model):
|
||||
class SuggestedProduct(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
name = db.Column(db.String(150), unique=True, nullable=False)
|
||||
|
||||
usage_count = db.Column(db.Integer, default=0)
|
||||
class Expense(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
list_id = db.Column(db.Integer, db.ForeignKey('shopping_list.id'))
|
||||
@@ -561,6 +561,39 @@ def suggest_products():
|
||||
suggestions = SuggestedProduct.query.filter(SuggestedProduct.name.ilike(f'%{query}%')).limit(5).all()
|
||||
return {'suggestions': [s.name for s in suggestions]}
|
||||
|
||||
@app.route('/all_products')
|
||||
def all_products():
|
||||
query = request.args.get('q', '')
|
||||
|
||||
top_products_query = SuggestedProduct.query
|
||||
if query:
|
||||
top_products_query = top_products_query.filter(SuggestedProduct.name.ilike(f'%{query}%'))
|
||||
top_products = (
|
||||
top_products_query
|
||||
.order_by(SuggestedProduct.usage_count.desc(), SuggestedProduct.name.asc())
|
||||
.limit(20)
|
||||
.all()
|
||||
)
|
||||
|
||||
top_names = [s.name for s in top_products]
|
||||
rest_query = SuggestedProduct.query
|
||||
if query:
|
||||
rest_query = rest_query.filter(SuggestedProduct.name.ilike(f'%{query}%'))
|
||||
|
||||
if top_names:
|
||||
rest_query = rest_query.filter(~SuggestedProduct.name.in_(top_names))
|
||||
|
||||
rest_products = (
|
||||
rest_query
|
||||
.order_by(SuggestedProduct.name.asc())
|
||||
.limit(200)
|
||||
.all()
|
||||
)
|
||||
|
||||
all_names = top_names + [s.name for s in rest_products]
|
||||
|
||||
return {'allproducts': all_names}
|
||||
|
||||
@app.route('/upload_receipt/<int:list_id>', methods=['POST'])
|
||||
def upload_receipt(list_id):
|
||||
if 'receipt' not in request.files:
|
||||
@@ -596,12 +629,6 @@ def uploaded_file(filename):
|
||||
response.headers['Content-Type'] = mime
|
||||
return response
|
||||
|
||||
@app.route('/all_products')
|
||||
@login_required
|
||||
def all_products():
|
||||
suggestions = SuggestedProduct.query.order_by(SuggestedProduct.name).all()
|
||||
return jsonify([s.name for s in suggestions])
|
||||
|
||||
@app.route('/admin')
|
||||
@login_required
|
||||
@admin_required
|
||||
|
@@ -1,38 +1,77 @@
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const modal = document.getElementById('massAddModal');
|
||||
const productList = document.getElementById('mass-add-list');
|
||||
let addedProducts = new Set();
|
||||
|
||||
document.querySelectorAll('#items li').forEach(li => {
|
||||
if (li.dataset.name) addedProducts.add(li.dataset.name.toLowerCase());
|
||||
});
|
||||
|
||||
modal.addEventListener('show.bs.modal', async function () {
|
||||
// 🔥 Za każdym razem od nowa budujemy zbiór produktów już na liście
|
||||
let addedProducts = new Set();
|
||||
document.querySelectorAll('#items li').forEach(li => {
|
||||
if (li.dataset.name) {
|
||||
addedProducts.add(li.dataset.name.toLowerCase());
|
||||
}
|
||||
});
|
||||
|
||||
productList.innerHTML = '<li class="list-group-item bg-dark text-light">Ładowanie...</li>';
|
||||
try {
|
||||
const res = await fetch('/all_products');
|
||||
const suggestions = await res.json();
|
||||
const data = await res.json();
|
||||
const allproducts = data.allproducts;
|
||||
productList.innerHTML = '';
|
||||
suggestions.forEach(name => {
|
||||
allproducts.forEach(name => {
|
||||
const li = document.createElement('li');
|
||||
li.className = 'list-group-item d-flex justify-content-between align-items-center bg-dark text-light';
|
||||
li.textContent = name;
|
||||
|
||||
if (addedProducts.has(name.toLowerCase())) {
|
||||
li.classList.add('active');
|
||||
li.innerHTML += '<span class="ms-2 text-success">✓</span>';
|
||||
// Produkt już dodany — oznacz jako nieaktywny
|
||||
li.classList.add('opacity-50');
|
||||
const badge = document.createElement('span');
|
||||
badge.className = 'badge bg-success ms-auto';
|
||||
badge.textContent = 'Dodano';
|
||||
li.appendChild(badge);
|
||||
} else {
|
||||
// Pole do ilości
|
||||
// Kontener na minus, pole i plus
|
||||
const qtyWrapper = document.createElement('div');
|
||||
qtyWrapper.className = 'd-flex align-items-center ms-2';
|
||||
|
||||
// Minus
|
||||
const minusBtn = document.createElement('button');
|
||||
minusBtn.type = 'button';
|
||||
minusBtn.className = 'btn btn-outline-light btn-sm px-2';
|
||||
minusBtn.textContent = '−';
|
||||
minusBtn.onclick = () => {
|
||||
qty.value = Math.max(1, parseInt(qty.value) - 1);
|
||||
};
|
||||
|
||||
// Pole ilości
|
||||
const qty = document.createElement('input');
|
||||
qty.type = 'number';
|
||||
qty.min = 1;
|
||||
qty.value = 1;
|
||||
qty.className = 'quantity-input ms-2';
|
||||
qty.className = 'form-control text-center p-1';
|
||||
qty.classList.add('rounded');
|
||||
qty.style.width = '50px';
|
||||
qty.style.flex = '0 0 auto';
|
||||
qty.style.margin = '0 2px';
|
||||
qty.title = 'Ilość';
|
||||
|
||||
// Plus
|
||||
const plusBtn = document.createElement('button');
|
||||
plusBtn.type = 'button';
|
||||
plusBtn.className = 'btn btn-outline-light btn-sm px-2';
|
||||
plusBtn.textContent = '+';
|
||||
plusBtn.onclick = () => {
|
||||
qty.value = parseInt(qty.value) + 1;
|
||||
};
|
||||
|
||||
// Dodajemy przyciski i input do wrappera
|
||||
qtyWrapper.appendChild(minusBtn);
|
||||
qtyWrapper.appendChild(qty);
|
||||
qtyWrapper.appendChild(plusBtn);
|
||||
|
||||
// Przycisk dodania
|
||||
const btn = document.createElement('button');
|
||||
btn.className = 'btn btn-sm btn-primary add-btn';
|
||||
btn.className = 'btn btn-sm btn-primary ms-2';
|
||||
btn.textContent = '+';
|
||||
btn.onclick = () => {
|
||||
const quantity = parseInt(qty.value) || 1;
|
||||
@@ -40,7 +79,7 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
};
|
||||
|
||||
li.textContent = name;
|
||||
li.appendChild(qty);
|
||||
li.appendChild(qtyWrapper);
|
||||
li.appendChild(btn);
|
||||
}
|
||||
productList.appendChild(li);
|
||||
@@ -50,39 +89,32 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
}
|
||||
});
|
||||
|
||||
// 🔥 Aktualizacja na żywo po dodaniu
|
||||
socket.on('item_added', data => {
|
||||
document.querySelectorAll('#mass-add-list li').forEach(li => {
|
||||
if (li.textContent.trim().startsWith(data.name) && !li.classList.contains('active')) {
|
||||
li.classList.add('active');
|
||||
li.innerHTML = `${data.name} <span class="ms-2 text-success">✓</span>`;
|
||||
const itemName = li.firstChild.textContent.trim();
|
||||
|
||||
if (itemName === data.name && !li.classList.contains('opacity-50')) {
|
||||
// Usuń wszystkie dzieci
|
||||
while (li.firstChild) {
|
||||
li.removeChild(li.firstChild);
|
||||
}
|
||||
|
||||
// Ustaw nazwę
|
||||
li.textContent = data.name;
|
||||
|
||||
// Dodaj klasę wyszarzenia
|
||||
li.classList.add('opacity-50');
|
||||
|
||||
// Dodaj badge
|
||||
const badge = document.createElement('span');
|
||||
badge.className = 'badge bg-success ms-auto';
|
||||
badge.textContent = 'Dodano';
|
||||
li.appendChild(badge);
|
||||
|
||||
// Zablokuj kliknięcia
|
||||
li.onclick = null;
|
||||
}
|
||||
});
|
||||
|
||||
const itemsContainer = document.getElementById('items');
|
||||
if (!itemsContainer) return;
|
||||
if (document.getElementById(`item-${data.id}`)) return;
|
||||
|
||||
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}`;
|
||||
li.dataset.name = data.name;
|
||||
|
||||
let quantityBadge = '';
|
||||
if (data.quantity && data.quantity > 1) {
|
||||
quantityBadge = `<span class="badge bg-secondary ms-2">${data.quantity}×</span>`;
|
||||
}
|
||||
|
||||
li.innerHTML = `
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="check-${data.id}">
|
||||
<label class="form-check-label" for="check-${data.id}">
|
||||
${data.name}
|
||||
</label>
|
||||
${quantityBadge}
|
||||
</div>
|
||||
`;
|
||||
itemsContainer.appendChild(li);
|
||||
addedProducts.add(data.name.toLowerCase());
|
||||
});
|
||||
});
|
||||
|
@@ -74,7 +74,7 @@ Lista: <strong>{{ list.title }}</strong>
|
||||
|
||||
<ul id="items" class="list-group mb-3">
|
||||
{% for item in items %}
|
||||
<li class="list-group-item d-flex justify-content-between align-items-center flex-wrap {% if item.purchased %}bg-success text-white{% else %}item-not-checked{% endif %}" id="item-{{ item.id }}">
|
||||
<li data-name="{{ item.name|lower }}" id="item-{{ item.id }}" class="list-group-item d-flex justify-content-between align-items-center flex-wrap {% if item.purchased %}bg-success text-white{% else %}item-not-checked{% endif %}" id="item-{{ item.id }}">
|
||||
<div class="d-flex align-items-center flex-wrap gap-2 flex-grow-1">
|
||||
<input class="large-checkbox" 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 %}">
|
||||
|
Reference in New Issue
Block a user