nowa funckcja zmiana kolejnosci produktów

This commit is contained in:
Mateusz Gruszczyński committed 2025-07-18 12:09:21 +02:00
1 parent 804b80bbf5
commit 8c9f0f1a6a
8 files changed
+159 -13

No files matched your search

+5 -2
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;
+32 -5
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:
+5 -3
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>
+15
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
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
View File
@@ -0,0 +1,2 @@
/*! Sortable 1.15.6 - MIT | git://github.com/SortableJS/Sortable.git */
!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t=t||self).Sortable=e()}(this,function(){"use strict";function e(e,t){var n,o=Object.keys(e);return Object.getOwnPropertySymbols&&(n=Object.getOwnPropertySymbols(e),t&&(n=n.filter(function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable})),o.push.apply(o,n)),o}function I(o){for(var t=1;t<arguments.length;t++){var i=null!=arguments[t]?arguments[t]:{};t%2?e(Object(i),!0).forEach(function(t){var e,n;e=o,t=i[n=t],n in e?Object.defineProperty(e,n,{value:t,enumerable:!0,configurable:!0,writable:!0}):e[n]=t}):Object.getOwnPropertyDescriptors?Object.defineProperties(o,Object.getOwnPropertyDescriptors(i)):e(Object(i)).forEach(function(t){Object.defineProperty(o,t,Object.getOwnPropertyDescriptor(i,t))})}return o}function o(t){return(o="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(t)}function a(){return(a=Object.assign||function(t){for(var e=1;e<arguments.length;e++){var n,o=arguments[e];for(n in o)Object.prototype.hasOwnProperty.call(o,n)&&(t[n]=o[n])}return t}).apply(this,arguments)}function i(t,e){if(null==t)return{};var n,o=function(t,e){if(null==t)return{};for(var n,o={},i=Object.keys(t),r=0;r<i.length;r++)n=i[r],0<=e.indexOf(n)||(o[n]=t[n]);return o}(t,e);if(Object.getOwnPropertySymbols)for(var i=Object.getOwnPropertySymbols(t),r=0;r<i.length;r++)n=i[r],0<=e.indexOf(n)||Object.prototype.propertyIsEnumerable.call(t,n)&&(o[n]=t[n]);return o}function r(t){return function(t){if(Array.isArray(t))return l(t)}(t)||function(t){if("undefined"!=typeof Symbol&&null!=t[Symbol.iterator]||null!=t["@@iterator"])return Array.from(t)}(t)||function(t,e){if(t){if("string"==typeof t)return l(t,e);var n=Object.prototype.toString.call(t).slice(8,-1);return"Map"===(n="Object"===n&&t.constructor?t.constructor.name:n)||"Set"===n?Array.from(t):"Arguments"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)?l(t,e):void 0}}(t)||function(){throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}function l(t,e){(null==e||e>t.length)&&(e=t.length);for(var n=0,o=new Array(e);n<e;n++)o[n]=t[n];return o}function t(t){if("undefined"!=typeof window&&window.navigator)return!!navigator.userAgent.match(t)}var y=t(/(?:Trident.*rv[ :]?11\.|msie|iemobile|Windows Phone)/i),w=t(/Edge/i),s=t(/firefox/i),u=t(/safari/i)&&!t(/chrome/i)&&!t(/android/i),c=t(/iP(ad|od|hone)/i),n=t(/chrome/i)&&t(/android/i),d={capture:!1,passive:!1};function h(t,e,n){t.addEventListener(e,n,!y&&d)}function p(t,e,n){t.removeEventListener(e,n,!y&&d)}function f(t,e){if(e&&(">"===e[0]&&(e=e.substring(1)),t))try{if(t.matches)return t.matches(e);if(t.msMatchesSelector)return t.msMatchesSelector(e);if(t.webkitMatchesSelector)return t.webkitMatchesSelector(e)}catch(t){return}}function g(t){return t.host&&t!==document&&t.host.nodeType?t.host:t.parentNode}function P(t,e,n,o){if(t){n=n||document;do{if(null!=e&&(">"!==e[0]||t.parentNode===n)&&f(t,e)||o&&t===n)return t}while(t!==n&&(t=g(t)))}return null}var m,v=/\s+/g;function k(t,e,n){var o;t&&e&&(t.classList?t.classList[n?"add":"remove"](e):(o=(" "+t.className+" ").replace(v," ").replace(" "+e+" "," "),t.className=(o+(n?" "+e:"")).replace(v," ")))}function R(t,e,n){var o=t&&t.style;if(o){if(void 0===n)return document.defaultView&&document.defaultView.getComputedStyle?n=document.defaultView.getComputedStyle(t,""):t.currentStyle&&(n=t.currentStyle),void 0===e?n:n[e];o[e=!(e in o||-1!==e.indexOf("webkit"))?"-webkit-"+e:e]=n+("string"==typeof n?"":"px")}}function b(t,e){var n="";if("string"==typeof t)n=t;else do{var o=R(t,"transform")}while(o&&"none"!==o&&(n=o+" "+n),!e&&(t=t.parentNode));var i=window.DOMMatrix||window.WebKitCSSMatrix||window.CSSMatrix||window.MSCSSMatrix;return i&&new i(n)}function D(t,e,n){if(t){var o=t.getElementsByTagName(e),i=0,r=o.length;if(n)for(;i<r;i++)n(o[i],i);return o}return[]}function O(){var t=document.scrollingElement;return t||document.documentElement}function X(t,e,n,o,i){if(t.getBoundingClientRect||t===window){var r,a,l,s,c,u,d=t!==window&&t.parentNode&&t!==O()?(a=(r=t.getBoundingClientRect()).top,l=r.left,s=r.bottom,c=r.right,u=r.height,r.width):(l=a=0,s=window.innerHeight,c=window.innerWidth,u=window.innerHeight,window.innerWidth);if((e||n)&&t!==window&&(i=i||t.parentNode,!y))do{if(i&&i.getBoundingClientRect&&("none"!==R(i,"transform")||n&&"static"!==R(i,"position"))){var h=i.getBoundingClientRect();a-=h.top+parseInt(R(i,"border-top-width")),l-=h.left+parseInt(R(i,"border-left-width")),s=a+r.height,c=l+r.width;break}}while(i=i.parentNode);return o&&t!==window&&(o=(e=b(i||t))&&e.a,t=e&&e.d,e&&(s=(a/=t)+(u/=t),c=(l/=o)+(d/=o))),{top:a,left:l,bottom:s,right:c,width:d,height:u}}}function Y(t,e,n){for(var o=M(tLine truncated
+12 -3
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>
+5
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>