funkcja_niekupione #2

Merged
gru merged 14 commits from funkcja_niekupione into master 2025-07-18 22:07:29 +02:00
8 changed files with 159 additions and 13 deletions
Showing only changes of commit 8c9f0f1a6a - Show all commits

View File

@@ -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
View File

@@ -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:

View File

@@ -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>

View File

@@ -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
View 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

File diff suppressed because one or more lines are too long

View File

@@ -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>

View File

@@ -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>