nowa funckcja zmiana kolejnosci produktów
This commit is contained in:
@@ -28,9 +28,12 @@ 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
|
||||
# licznik najczesciej kupowanych reczy
|
||||
ALTER TABLE suggested_product ADD COLUMN usage_count INTEGER DEFAULT 0;
|
||||
|
||||
#funkcja niekupione
|
||||
# funkcja niekupione
|
||||
ALTER TABLE item ADD COLUMN not_purchased_reason TEXT;
|
||||
ALTER TABLE item ADD COLUMN not_purchased BOOLEAN DEFAULT 0;
|
||||
|
||||
# funkcja sortowania
|
||||
ALTER TABLE item ADD COLUMN position INTEGER DEFAULT 0;
|
||||
|
37
app.py
37
app.py
@@ -123,6 +123,7 @@ class Item(db.Model):
|
||||
note = db.Column(db.Text, nullable=True)
|
||||
not_purchased = db.Column(db.Boolean, default=False)
|
||||
not_purchased_reason = db.Column(db.Text, nullable=True)
|
||||
position = db.Column(db.Integer, default=0)
|
||||
|
||||
|
||||
class SuggestedProduct(db.Model):
|
||||
@@ -211,7 +212,7 @@ def allowed_file(filename):
|
||||
|
||||
def get_list_details(list_id):
|
||||
shopping_list = ShoppingList.query.get_or_404(list_id)
|
||||
items = Item.query.filter_by(list_id=list_id).all()
|
||||
items = Item.query.filter_by(list_id=list_id).order_by(Item.position.asc()).all()
|
||||
receipt_pattern = f"list_{list_id}"
|
||||
all_files = os.listdir(app.config["UPLOAD_FOLDER"])
|
||||
receipt_files = [f for f in all_files if receipt_pattern in f]
|
||||
@@ -220,6 +221,7 @@ def get_list_details(list_id):
|
||||
return shopping_list, items, receipt_files, expenses, total_expense
|
||||
|
||||
|
||||
|
||||
def generate_share_token(length=8):
|
||||
"""Generuje token do udostępniania. Parametr `length` to liczba znaków (domyślnie 4)."""
|
||||
return secrets.token_hex(length // 2)
|
||||
@@ -265,7 +267,7 @@ def admin_required(f):
|
||||
|
||||
|
||||
def get_progress(list_id):
|
||||
items = Item.query.filter_by(list_id=list_id).all()
|
||||
items = Item.query.filter_by(list_id=list_id).order_by(Item.position.asc()).all()
|
||||
total_count = len(items)
|
||||
purchased_count = len([i for i in items if i.purchased])
|
||||
percent = (purchased_count / total_count * 100) if total_count > 0 else 0
|
||||
@@ -980,6 +982,27 @@ def uploaded_file(filename):
|
||||
return response
|
||||
|
||||
|
||||
@app.route('/reorder_items', methods=['POST'])
|
||||
@login_required
|
||||
def reorder_items():
|
||||
data = request.get_json()
|
||||
list_id = data.get('list_id')
|
||||
order = data.get('order')
|
||||
|
||||
for index, item_id in enumerate(order):
|
||||
item = db.session.get(Item, item_id)
|
||||
if item and item.list_id == list_id:
|
||||
item.position = index
|
||||
db.session.commit()
|
||||
|
||||
socketio.emit("items_reordered", {
|
||||
"list_id": list_id,
|
||||
"order": order
|
||||
}, to=str(list_id))
|
||||
|
||||
return jsonify(success=True)
|
||||
|
||||
|
||||
@app.route("/admin")
|
||||
@login_required
|
||||
@admin_required
|
||||
@@ -1737,7 +1760,6 @@ def handle_add_item(data):
|
||||
except:
|
||||
quantity = 1
|
||||
|
||||
# Szukamy istniejącego itemu w tej liście (ignorując wielkość liter)
|
||||
existing_item = Item.query.filter(
|
||||
Item.list_id == list_id,
|
||||
func.lower(Item.name) == name.lower(),
|
||||
@@ -1758,10 +1780,15 @@ def handle_add_item(data):
|
||||
to=str(list_id),
|
||||
)
|
||||
else:
|
||||
max_position = db.session.query(func.max(Item.position)).filter_by(list_id=list_id).scalar()
|
||||
if max_position is None:
|
||||
max_position = 0
|
||||
|
||||
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,
|
||||
)
|
||||
db.session.add(new_item)
|
||||
@@ -1788,7 +1815,6 @@ def handle_add_item(data):
|
||||
include_self=True,
|
||||
)
|
||||
|
||||
# Aktualizacja postępu
|
||||
purchased_count, total_count, percent = get_progress(list_id)
|
||||
|
||||
emit(
|
||||
@@ -1802,6 +1828,7 @@ def handle_add_item(data):
|
||||
)
|
||||
|
||||
|
||||
|
||||
@socketio.on("check_item")
|
||||
def handle_check_item(data):
|
||||
# item = Item.query.get(data["item_id"])
|
||||
@@ -1855,7 +1882,7 @@ 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).all()
|
||||
items = Item.query.filter_by(list_id=list_id).order_by(Item.position.asc()).all()
|
||||
|
||||
items_data = []
|
||||
for item in items:
|
||||
|
@@ -179,7 +179,7 @@ function openList(link) {
|
||||
}
|
||||
|
||||
function applyHidePurchased(isInit = false) {
|
||||
console.log("applyHidePurchased: wywołana, isInit =", isInit);
|
||||
//console.log("applyHidePurchased: wywołana, isInit =", isInit);
|
||||
const toggle = document.getElementById('hidePurchasedToggle');
|
||||
if (!toggle) return;
|
||||
const hide = toggle.checked;
|
||||
@@ -273,6 +273,7 @@ function isListDifferent(oldItems, newItems) {
|
||||
}
|
||||
|
||||
function updateListSmoothly(newItems) {
|
||||
|
||||
const itemsContainer = document.getElementById('items');
|
||||
const existingItemsMap = new Map();
|
||||
|
||||
@@ -292,7 +293,6 @@ function updateListSmoothly(newItems) {
|
||||
|
||||
if (!li) {
|
||||
li = document.createElement('li');
|
||||
li.className = `list-group-item d-flex justify-content-between align-items-center flex-wrap clickable-item`;
|
||||
li.id = `item-${item.id}`;
|
||||
}
|
||||
|
||||
@@ -301,9 +301,10 @@ function updateListSmoothly(newItems) {
|
||||
item.not_purchased ? 'bg-warning text-dark' : 'item-not-checked'
|
||||
}`;
|
||||
|
||||
// HTML wewnętrzny
|
||||
// Wewnętrzny HTML
|
||||
li.innerHTML = `
|
||||
<div class="d-flex align-items-center gap-3 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' : ''}>
|
||||
@@ -311,6 +312,7 @@ function updateListSmoothly(newItems) {
|
||||
<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>
|
||||
|
@@ -103,6 +103,20 @@ socket.on('receipt_added', function (data) {
|
||||
}
|
||||
});
|
||||
|
||||
socket.on("items_reordered", data => {
|
||||
if (data.list_id !== window.LIST_ID) return;
|
||||
|
||||
if (window.currentItems) {
|
||||
window.currentItems = data.order.map(id =>
|
||||
window.currentItems.find(item => item.id === id)
|
||||
).filter(Boolean);
|
||||
|
||||
updateListSmoothly(window.currentItems);
|
||||
//showToast('Kolejność produktów zaktualizowana', 'info');
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
socket.on('full_list', function (data) {
|
||||
const itemsContainer = document.getElementById('items');
|
||||
|
||||
@@ -112,6 +126,7 @@ socket.on('full_list', function (data) {
|
||||
|
||||
const isDifferent = isListDifferent(oldItems, data.items);
|
||||
|
||||
window.currentItems = data.items;
|
||||
updateListSmoothly(data.items);
|
||||
toggleEmptyPlaceholder();
|
||||
|
||||
|
83
static/js/sort_mode.js
Normal file
83
static/js/sort_mode.js
Normal file
@@ -0,0 +1,83 @@
|
||||
let sortable = null;
|
||||
let isSorting = false;
|
||||
|
||||
function enableSortMode() {
|
||||
if (sortable || isSorting) return;
|
||||
isSorting = true;
|
||||
localStorage.setItem('sortModeEnabled', 'true');
|
||||
|
||||
const itemsContainer = document.getElementById('items');
|
||||
const listId = window.LIST_ID;
|
||||
|
||||
if (!itemsContainer || !listId) return;
|
||||
|
||||
sortable = Sortable.create(itemsContainer, {
|
||||
animation: 150,
|
||||
handle: '.drag-handle',
|
||||
ghostClass: 'drag-ghost',
|
||||
filter: 'input, button',
|
||||
preventOnFilter: false,
|
||||
onEnd: function () {
|
||||
const order = Array.from(itemsContainer.children)
|
||||
.map(li => parseInt(li.id.replace('item-', '')))
|
||||
.filter(id => !isNaN(id));
|
||||
|
||||
fetch('/reorder_items', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ list_id: listId, order })
|
||||
}).then(() => {
|
||||
showToast('Zapisano nową kolejność', 'success');
|
||||
|
||||
if (window.currentItems) {
|
||||
window.currentItems = order.map(id =>
|
||||
window.currentItems.find(item => item.id === id)
|
||||
);
|
||||
updateListSmoothly(window.currentItems);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const btn = document.getElementById('sort-toggle-btn');
|
||||
if (btn) {
|
||||
btn.textContent = '✔️ Zakończ sortowanie';
|
||||
btn.classList.remove('btn-outline-warning');
|
||||
btn.classList.add('btn-outline-success');
|
||||
}
|
||||
|
||||
if (window.currentItems) {
|
||||
updateListSmoothly(window.currentItems);
|
||||
}
|
||||
}
|
||||
|
||||
function disableSortMode() {
|
||||
if (sortable) {
|
||||
sortable.destroy();
|
||||
sortable = null;
|
||||
}
|
||||
isSorting = false;
|
||||
localStorage.removeItem('sortModeEnabled');
|
||||
|
||||
const btn = document.getElementById('sort-toggle-btn');
|
||||
if (btn) {
|
||||
btn.textContent = '✳️ Zmień kolejność';
|
||||
btn.classList.remove('btn-outline-success');
|
||||
btn.classList.add('btn-outline-warning');
|
||||
}
|
||||
|
||||
if (window.currentItems) {
|
||||
updateListSmoothly(window.currentItems);
|
||||
}
|
||||
}
|
||||
|
||||
function toggleSortMode() {
|
||||
isSorting ? disableSortMode() : enableSortMode();
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const wasSorting = localStorage.getItem('sortModeEnabled') === 'true';
|
||||
if (wasSorting) {
|
||||
enableSortMode();
|
||||
}
|
||||
});
|
2
static/lib/js/Sortable.min.js
vendored
Normal file
2
static/lib/js/Sortable.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@@ -79,9 +79,14 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="form-check form-switch mb-3 d-flex justify-content-end">
|
||||
<input class="form-check-input" type="checkbox" id="hidePurchasedToggle">
|
||||
<label class="form-check-label ms-2" for="hidePurchasedToggle">Ukryj zaznaczone</label>
|
||||
<div class="d-flex justify-content-between align-items-center mb-3 flex-wrap">
|
||||
<button id="sort-toggle-btn" class="btn btn-sm btn-outline-warning" onclick="toggleSortMode()">
|
||||
✳️ Zmień kolejność
|
||||
</button>
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" id="hidePurchasedToggle">
|
||||
<label class="form-check-label ms-2" for="hidePurchasedToggle">Ukryj zaznaczone</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ul id="items" class="list-group mb-3" data-is-share="{{ 'true' if is_share else 'false' }}">
|
||||
@@ -205,12 +210,16 @@
|
||||
</div>
|
||||
|
||||
{% block scripts %}
|
||||
<script src="{{ url_for('static_bp.serve_js_lib', filename='Sortable.min.js') }}"></script>
|
||||
<script>
|
||||
const isShare = document.getElementById('items').dataset.isShare === 'true';
|
||||
window.IS_SHARE = isShare;
|
||||
window.LIST_ID = {{ list.id }};
|
||||
</script>
|
||||
|
||||
<script src="{{ url_for('static_bp.serve_js', filename='mass_add.js') }}"></script>
|
||||
<script src="{{ url_for('static_bp.serve_js', filename='receipt_upload.js') }}"></script>
|
||||
<script src="{{ url_for('static_bp.serve_js', filename='sort_mode.js') }}"></script>
|
||||
<script>
|
||||
setupList({{ list.id }}, '{{ current_user.username if current_user.is_authenticated else 'Gość' }}');
|
||||
</script>
|
||||
|
@@ -170,7 +170,12 @@
|
||||
<script>
|
||||
const isShare = document.getElementById('items').dataset.isShare === 'true';
|
||||
window.IS_SHARE = isShare;
|
||||
window.LIST_ID = {{ list.id }};
|
||||
if (typeof isSorting === 'undefined') {
|
||||
var isSorting = false;
|
||||
}
|
||||
</script>
|
||||
<script src="{{ url_for('static_bp.serve_js_lib', filename='Sortable.min.js') }}"></script>
|
||||
<script src="{{ url_for('static_bp.serve_js', filename='notes.js') }}"></script>
|
||||
<script src="{{ url_for('static_bp.serve_js', filename='clickable_row.js') }}"></script>
|
||||
<script src="{{ url_for('static_bp.serve_js', filename='receipt_section.js') }}"></script>
|
||||
|
Reference in New Issue
Block a user