diff --git a/alters.txt b/alters.txt
index 28fe56e..1731e50 100644
--- a/alters.txt
+++ b/alters.txt
@@ -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;
diff --git a/app.py b/app.py
index 51d8e95..15fa1ea 100644
--- a/app.py
+++ b/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:
diff --git a/static/js/functions.js b/static/js/functions.js
index ac36d16..9d5cd7b 100644
--- a/static/js/functions.js
+++ b/static/js/functions.js
@@ -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 = `
+ ${isSorting ? `☰` : ''}
${!item.not_purchased ? `
@@ -311,6 +312,7 @@ function updateListSmoothly(newItems) {
🚫
`}
${item.name} ${quantityBadge}
+
${item.note ? `[ ${item.note} ]` : ''}
${item.not_purchased_reason ? `[ Powód: ${item.not_purchased_reason} ]` : ''}
diff --git a/static/js/sockets.js b/static/js/sockets.js
index 2a8f9a7..2a21f63 100644
--- a/static/js/sockets.js
+++ b/static/js/sockets.js
@@ -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();
diff --git a/static/js/sort_mode.js b/static/js/sort_mode.js
new file mode 100644
index 0000000..0a91843
--- /dev/null
+++ b/static/js/sort_mode.js
@@ -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();
+ }
+});
\ No newline at end of file
diff --git a/static/lib/js/Sortable.min.js b/static/lib/js/Sortable.min.js
new file mode 100644
index 0000000..95423a6
--- /dev/null
+++ b/static/lib/js/Sortable.min.js
@@ -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;tt.length)&&(e=t.length);for(var n=0,o=new Array(e);n"===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=n.left-e&&i<=n.right+e,e=r>=n.top-e&&r<=n.bottom+e;return o&&e?a=t:void 0}}),a);if(e){var n,o={};for(n in t)t.hasOwnProperty(n)&&(o[n]=t[n]);o.target=o.rootEl=e,o.preventDefault=void 0,o.stopPropagation=void 0,e[K]._onDragOver(o)}}var i,r,a}function Ft(t){Z&&Z.parentNode[K]._isOutsideThisEl(t.target)}function jt(t,e){if(!t||!t.nodeType||1!==t.nodeType)throw"Sortable: `el` must be an HTMLElement, not ".concat({}.toString.call(t));this.el=t,this.options=e=a({},e),t[K]=this;var n,o,i={group:null,sort:!0,disabled:!1,store:null,handle:null,draggable:/^[uo]l$/i.test(t.nodeName)?">li":">*",swapThreshold:1,invertSwap:!1,invertedSwapThreshold:null,removeCloneOnHide:!0,direction:function(){return kt(t,this.options)},ghostClass:"sortable-ghost",chosenClass:"sortable-chosen",dragClass:"sortable-drag",ignore:"a, img",filter:null,preventOnFilter:!0,animation:0,easing:null,setData:function(t,e){t.setData("Text",e.textContent)},dropBubble:!1,dragoverBubble:!1,dataIdAttr:"data-id",delay:0,delayOnTouchOnly:!1,touchStartThreshold:(Number.parseInt?Number:window).parseInt(window.devicePixelRatio,10)||1,forceFallback:!1,fallbackClass:"sortable-fallback",fallbackOnBody:!1,fallbackTolerance:0,fallbackOffset:{x:0,y:0},supportPointer:!1!==jt.supportPointer&&"PointerEvent"in window&&(!u||c),emptyInsertThreshold:5};for(n in z.initializePlugins(this,t,i),i)n in e||(e[n]=i[n]);for(o in Rt(e),this)"_"===o.charAt(0)&&"function"==typeof this[o]&&(this[o]=this[o].bind(this));this.nativeDraggable=!e.forceFallback&&It,this.nativeDraggable&&(this.options.touchStartThreshold=1),e.supportPointer?h(t,"pointerdown",this._onTapStart):(h(t,"mousedown",this._onTapStart),h(t,"touchstart",this._onTapStart)),this.nativeDraggable&&(h(t,"dragover",this),h(t,"dragenter",this)),St.push(this.el),e.store&&e.store.get&&this.sort(e.store.get(this)||[]),a(this,A())}function Ht(t,e,n,o,i,r,a,l){var s,c,u=t[K],d=u.options.onMove;return!window.CustomEvent||y||w?(s=document.createEvent("Event")).initEvent("move",!0,!0):s=new CustomEvent("move",{bubbles:!0,cancelable:!0}),s.to=e,s.from=t,s.dragged=n,s.draggedRect=o,s.related=i||e,s.relatedRect=r||X(e),s.willInsertAfter=l,s.originalEvent=a,t.dispatchEvent(s),c=d?d.call(u,s,a):c}function Lt(t){t.draggable=!1}function Kt(){xt=!1}function Wt(t){return setTimeout(t,0)}function zt(t){return clearTimeout(t)}jt.prototype={constructor:jt,_isOutsideThisEl:function(t){this.el.contains(t)||t===this.el||(vt=null)},_getDirection:function(t,e){return"function"==typeof this.options.direction?this.options.direction.call(this,t,e,Z):this.options.direction},_onTapStart:function(e){if(e.cancelable){var n=this,o=this.el,t=this.options,i=t.preventOnFilter,r=e.type,a=e.touches&&e.touches[0]||e.pointerType&&"touch"===e.pointerType&&e,l=(a||e).target,s=e.target.shadowRoot&&(e.path&&e.path[0]||e.composedPath&&e.composedPath()[0])||l,c=t.filter;if(!function(t){Ot.length=0;var e=t.getElementsByTagName("input"),n=e.length;for(;n--;){var o=e[n];o.checked&&Ot.push(o)}}(o),!Z&&!(/mousedown|pointerdown/.test(r)&&0!==e.button||t.disabled)&&!s.isContentEditable&&(this.nativeDraggable||!u||!l||"SELECT"!==l.tagName.toUpperCase())&&!((l=P(l,t.draggable,o,!1))&&l.animated||et===l)){if(it=j(l),at=j(l,t.draggable),"function"==typeof c){if(c.call(this,e,l,this))return V({sortable:n,rootEl:s,name:"filter",targetEl:l,toEl:o,fromEl:o}),U("filter",n,{evt:e}),void(i&&e.preventDefault())}else if(c=c&&c.split(",").some(function(t){if(t=P(s,t.trim(),o,!1))return V({sortable:n,rootEl:t,name:"filter",targetEl:l,fromEl:o,toEl:o}),U("filter",n,{evt:e}),!0}))return void(i&&e.preventDefault());t.handle&&!P(s,t.handle,o,!1)||this._prepareDragStart(e,a,l)}}},_prepareDragStart:function(t,e,n){var o,i=this,r=i.el,a=i.options,l=r.ownerDocument;n&&!Z&&n.parentNode===r&&(o=X(n),J=r,$=(Z=n).parentNode,tt=Z.nextSibling,et=n,st=a.group,ut={target:jt.dragged=Z,clientX:(e||t).clientX,clientY:(e||t).clientY},ft=ut.clientX-o.left,gt=ut.clientY-o.top,this._lastX=(e||t).clientX,this._lastY=(e||t).clientY,Z.style["will-change"]="all",o=function(){U("delayEnded",i,{evt:t}),jt.eventCanceled?i._onDrop():(i._disableDelayedDragEvents(),!s&&i.nativeDraggable&&(Z.draggable=!0),i._triggerDragStart(t,e),V({sortable:i,name:"choose",originalEvent:t}),k(Z,a.chosenClass,!0))},a.ignore.split(",").forEach(function(t){D(Z,t.trim(),Lt)}),h(l,"dragover",Bt),h(l,"mousemove",Bt),h(l,"touchmove",Bt),a.supportPointer?(h(l,"pointerup",i._onDrop),this.nativeDraggable||h(l,"pointercancel",i._onDrop)):(h(l,"mouseup",i._onDrop),h(l,"touchend",i._onDrop),h(l,"touchcancel",i._onDrop)),s&&this.nativeDraggable&&(this.options.touchStartThreshold=4,Z.draggable=!0),U("delayStart",this,{evt:t}),!a.delay||a.delayOnTouchOnly&&!e||this.nativeDraggable&&(w||y)?o():jt.eventCanceled?this._onDrop():(a.supportPointer?(h(l,"pointerup",i._disableDelayedDrag),h(l,"pointercancel",i._disableDelayedDrag)):(h(l,"mouseup",i._disableDelayedDrag),h(l,"touchend",i._disableDelayedDrag),h(l,"touchcancel",i._disableDelayedDrag)),h(l,"mousemove",i._delayedDragTouchMoveHandler),h(l,"touchmove",i._delayedDragTouchMoveHandler),a.supportPointer&&h(l,"pointermove",i._delayedDragTouchMoveHandler),i._dragStartTimer=setTimeout(o,a.delay)))},_delayedDragTouchMoveHandler:function(t){t=t.touches?t.touches[0]:t;Math.max(Math.abs(t.clientX-this._lastX),Math.abs(t.clientY-this._lastY))>=Math.floor(this.options.touchStartThreshold/(this.nativeDraggable&&window.devicePixelRatio||1))&&this._disableDelayedDrag()},_disableDelayedDrag:function(){Z&&Lt(Z),clearTimeout(this._dragStartTimer),this._disableDelayedDragEvents()},_disableDelayedDragEvents:function(){var t=this.el.ownerDocument;p(t,"mouseup",this._disableDelayedDrag),p(t,"touchend",this._disableDelayedDrag),p(t,"touchcancel",this._disableDelayedDrag),p(t,"pointerup",this._disableDelayedDrag),p(t,"pointercancel",this._disableDelayedDrag),p(t,"mousemove",this._delayedDragTouchMoveHandler),p(t,"touchmove",this._delayedDragTouchMoveHandler),p(t,"pointermove",this._delayedDragTouchMoveHandler)},_triggerDragStart:function(t,e){e=e||"touch"==t.pointerType&&t,!this.nativeDraggable||e?this.options.supportPointer?h(document,"pointermove",this._onTouchMove):h(document,e?"touchmove":"mousemove",this._onTouchMove):(h(Z,"dragend",this),h(J,"dragstart",this._onDragStart));try{document.selection?Wt(function(){document.selection.empty()}):window.getSelection().removeAllRanges()}catch(t){}},_dragStarted:function(t,e){var n;Dt=!1,J&&Z?(U("dragStarted",this,{evt:e}),this.nativeDraggable&&h(document,"dragover",Ft),n=this.options,t||k(Z,n.dragClass,!1),k(Z,n.ghostClass,!0),jt.active=this,t&&this._appendGhost(),V({sortable:this,name:"start",originalEvent:e})):this._nulling()},_emulateDragOver:function(){if(dt){this._lastX=dt.clientX,this._lastY=dt.clientY,Xt();for(var t=document.elementFromPoint(dt.clientX,dt.clientY),e=t;t&&t.shadowRoot&&(t=t.shadowRoot.elementFromPoint(dt.clientX,dt.clientY))!==e;)e=t;if(Z.parentNode[K]._isOutsideThisEl(t),e)do{if(e[K])if(e[K]._onDragOver({clientX:dt.clientX,clientY:dt.clientY,target:t,rootEl:e})&&!this.options.dragoverBubble)break}while(e=g(t=e));Yt()}},_onTouchMove:function(t){if(ut){var e=this.options,n=e.fallbackTolerance,o=e.fallbackOffset,i=t.touches?t.touches[0]:t,r=Q&&b(Q,!0),a=Q&&r&&r.a,l=Q&&r&&r.d,e=At&&wt&&E(wt),a=(i.clientX-ut.clientX+o.x)/(a||1)+(e?e[0]-Tt[0]:0)/(a||1),l=(i.clientY-ut.clientY+o.y)/(l||1)+(e?e[1]-Tt[1]:0)/(l||1);if(!jt.active&&!Dt){if(n&&Math.max(Math.abs(i.clientX-this._lastX),Math.abs(i.clientY-this._lastY))E.right+10||S.clientY>x.bottom&&S.clientX>x.left:S.clientY>E.bottom+10||S.clientX>x.right&&S.clientY>x.top)||m.animated)){if(m&&(t=n,e=r,C=X(B((_=this).el,0,_.options,!0)),_=L(_.el,_.options,Q),e?t.clientX<_.left-10||t.clientY
{% endif %}
-
-
-
+
+
+
+
+
+
{% block scripts %}
+
+
+
diff --git a/templates/list_share.html b/templates/list_share.html
index f20b0e0..b7af618 100644
--- a/templates/list_share.html
+++ b/templates/list_share.html
@@ -170,7 +170,12 @@
+