Jednostki #16
@@ -249,6 +249,8 @@ def duplicate_list_for_schedule(source_list: ShoppingList, scheduled_for: dateti
|
||||
list_id=new_list.id,
|
||||
name=item.name,
|
||||
quantity=item.quantity or 1,
|
||||
quantity_value=getattr(item, "quantity_value", None) or item.quantity or 1,
|
||||
quantity_unit=normalize_quantity_unit(getattr(item, "quantity_unit", None)),
|
||||
note=item.note,
|
||||
position=item.position or 0,
|
||||
added_at=scheduled_for,
|
||||
@@ -365,6 +367,8 @@ def create_list_from_template_at_schedule(template: ListTemplate, owner: User, s
|
||||
list_id=new_list.id,
|
||||
name=item.name,
|
||||
quantity=item.quantity or 1,
|
||||
quantity_value=getattr(item, "quantity_value", None) or item.quantity or 1,
|
||||
quantity_unit=normalize_quantity_unit(getattr(item, "quantity_unit", None)),
|
||||
note=item.note,
|
||||
position=item.position or idx,
|
||||
added_by=owner.id,
|
||||
@@ -574,6 +578,8 @@ def create_template_from_list(source_list: ShoppingList, created_by: int | None
|
||||
template_id=template.id,
|
||||
name=item.name,
|
||||
quantity=item.quantity or 1,
|
||||
quantity_value=getattr(item, "quantity_value", None) or item.quantity or 1,
|
||||
quantity_unit=normalize_quantity_unit(getattr(item, "quantity_unit", None)),
|
||||
note=item.note,
|
||||
position=idx + 1,
|
||||
))
|
||||
@@ -606,6 +612,8 @@ def create_list_from_template(template: ListTemplate, owner: User, title: str |
|
||||
list_id=new_list.id,
|
||||
name=item.name,
|
||||
quantity=item.quantity or 1,
|
||||
quantity_value=getattr(item, "quantity_value", None) or item.quantity or 1,
|
||||
quantity_unit=normalize_quantity_unit(getattr(item, "quantity_unit", None)),
|
||||
note=item.note,
|
||||
position=idx + 1,
|
||||
added_by=owner.id,
|
||||
@@ -641,6 +649,87 @@ def parse_api_date_range(start_date_str: str | None, end_date_str: str | None):
|
||||
return start_date, end_date
|
||||
|
||||
|
||||
|
||||
|
||||
def normalize_quantity_unit(unit):
|
||||
unit = (unit or "szt").strip().lower()
|
||||
aliases = {
|
||||
"szt.": "szt", "sztuki": "szt", "sztuka": "szt", "pcs": "szt",
|
||||
"kilogram": "kg", "kilogramy": "kg",
|
||||
"gram": "g", "gramy": "g",
|
||||
"litr": "l", "litry": "l",
|
||||
"mililitr": "ml", "mililitry": "ml",
|
||||
"opak": "opak.", "op": "opak.", "opakowanie": "opak.", "opakowania": "opak.",
|
||||
}
|
||||
unit = aliases.get(unit, unit)
|
||||
allowed = {"szt", "kg", "g", "l", "ml", "opak."}
|
||||
return unit if unit in allowed else "szt"
|
||||
|
||||
|
||||
def parse_quantity_value(value, fallback=1):
|
||||
try:
|
||||
raw = str(value if value is not None else fallback).strip().replace(",", ".")
|
||||
parsed = float(raw)
|
||||
if parsed <= 0:
|
||||
parsed = float(fallback or 1)
|
||||
except Exception:
|
||||
parsed = float(fallback or 1)
|
||||
return max(parsed, 0.001)
|
||||
|
||||
|
||||
def legacy_quantity_int(value):
|
||||
try:
|
||||
parsed = int(round(float(value)))
|
||||
except Exception:
|
||||
parsed = 1
|
||||
return max(parsed, 1)
|
||||
|
||||
|
||||
def quantity_display_value(item):
|
||||
value = getattr(item, "quantity_value", None)
|
||||
if value is None:
|
||||
value = getattr(item, "quantity", 1) or 1
|
||||
try:
|
||||
value = float(value)
|
||||
except Exception:
|
||||
value = 1.0
|
||||
if value.is_integer():
|
||||
return str(int(value))
|
||||
return (f"{value:.3f}".rstrip("0").rstrip(".")).replace(".", ",")
|
||||
|
||||
|
||||
def quantity_label(item):
|
||||
unit = normalize_quantity_unit(getattr(item, "quantity_unit", None))
|
||||
value = quantity_display_value(item)
|
||||
if unit == "szt":
|
||||
return f"x{value}"
|
||||
return f"{value} {unit}"
|
||||
|
||||
|
||||
def ensure_lightweight_schema_migrations():
|
||||
inspector = inspect(db.engine)
|
||||
table_names = set(inspector.get_table_names())
|
||||
targets = []
|
||||
for table_name in ("item", "list_template_item"):
|
||||
if table_name not in table_names:
|
||||
continue
|
||||
existing = {c["name"] for c in inspector.get_columns(table_name)}
|
||||
if "quantity_value" not in existing:
|
||||
targets.append((table_name, f"ALTER TABLE {table_name} ADD COLUMN quantity_value FLOAT"))
|
||||
if "quantity_unit" not in existing:
|
||||
targets.append((table_name, f"ALTER TABLE {table_name} ADD COLUMN quantity_unit VARCHAR(20) DEFAULT 'szt'"))
|
||||
if not targets:
|
||||
return
|
||||
with db.engine.begin() as conn:
|
||||
touched_tables = set()
|
||||
for table_name, stmt in targets:
|
||||
conn.execute(text(stmt))
|
||||
touched_tables.add(table_name)
|
||||
for table_name in touched_tables:
|
||||
conn.execute(text(f"UPDATE {table_name} SET quantity_value = quantity WHERE quantity_value IS NULL"))
|
||||
conn.execute(text(f"UPDATE {table_name} SET quantity_unit = 'szt' WHERE quantity_unit IS NULL OR quantity_unit = ''"))
|
||||
print("[INFO] Zastosowano lekką migrację schematu dla ilości/jednostek produktów")
|
||||
|
||||
def set_authorized_cookie(response):
|
||||
secure_flag = app.config["SESSION_COOKIE_SECURE"]
|
||||
max_age = app.config.get("AUTH_COOKIE_MAX_AGE", 86400)
|
||||
@@ -664,6 +753,7 @@ if app.config["SQLALCHEMY_DATABASE_URI"].startswith("sqlite:///"):
|
||||
|
||||
with app.app_context():
|
||||
db.create_all()
|
||||
ensure_lightweight_schema_migrations()
|
||||
|
||||
# --- Tworzenie admina ---
|
||||
admin_username = DEFAULT_ADMIN_USERNAME
|
||||
|
||||
@@ -73,6 +73,8 @@ class Item(db.Model):
|
||||
purchased = db.Column(db.Boolean, default=False)
|
||||
purchased_at = db.Column(db.DateTime, nullable=True)
|
||||
quantity = db.Column(db.Integer, default=1)
|
||||
quantity_value = db.Column(db.Float, nullable=True)
|
||||
quantity_unit = db.Column(db.String(20), default="szt", nullable=True)
|
||||
note = db.Column(db.Text, nullable=True)
|
||||
not_purchased = db.Column(db.Boolean, default=False)
|
||||
not_purchased_reason = db.Column(db.Text, nullable=True)
|
||||
@@ -186,6 +188,8 @@ class ListTemplateItem(db.Model):
|
||||
template_id = db.Column(db.Integer, db.ForeignKey("list_template.id", ondelete="CASCADE"), nullable=False)
|
||||
name = db.Column(db.String(150), nullable=False)
|
||||
quantity = db.Column(db.Integer, default=1)
|
||||
quantity_value = db.Column(db.Float, nullable=True)
|
||||
quantity_unit = db.Column(db.String(20), default="szt", nullable=True)
|
||||
note = db.Column(db.Text, nullable=True)
|
||||
position = db.Column(db.Integer, default=0)
|
||||
|
||||
|
||||
+36
-21
@@ -191,25 +191,27 @@ def handle_edit_item(data):
|
||||
item = db.session.get(Item, data["item_id"])
|
||||
|
||||
new_name = data["new_name"]
|
||||
new_quantity = data.get("new_quantity", item.quantity)
|
||||
new_quantity_value = parse_quantity_value(data.get("quantity_value", data.get("new_quantity", item.quantity)))
|
||||
new_quantity_unit = normalize_quantity_unit(data.get("quantity_unit", getattr(item, "quantity_unit", None)))
|
||||
|
||||
if item and new_name.strip():
|
||||
item.name = new_name.strip()
|
||||
|
||||
try:
|
||||
new_quantity = int(new_quantity)
|
||||
if new_quantity < 1:
|
||||
new_quantity = 1
|
||||
except:
|
||||
new_quantity = 1
|
||||
|
||||
item.quantity = new_quantity
|
||||
item.quantity_value = new_quantity_value
|
||||
item.quantity_unit = new_quantity_unit
|
||||
item.quantity = legacy_quantity_int(new_quantity_value)
|
||||
|
||||
db.session.commit()
|
||||
|
||||
emit(
|
||||
"item_edited",
|
||||
{"item_id": item.id, "new_name": item.name, "new_quantity": item.quantity},
|
||||
{
|
||||
"item_id": item.id,
|
||||
"new_name": item.name,
|
||||
"new_quantity": item.quantity,
|
||||
"quantity_value": item.quantity_value,
|
||||
"quantity_unit": item.quantity_unit,
|
||||
"quantity_label": quantity_label(item),
|
||||
},
|
||||
to=str(item.list_id),
|
||||
)
|
||||
|
||||
@@ -249,19 +251,14 @@ def handle_disconnect(sid):
|
||||
def handle_add_item(data):
|
||||
list_id = data["list_id"]
|
||||
name = data["name"].strip()
|
||||
quantity = data.get("quantity", 1)
|
||||
quantity_value = parse_quantity_value(data.get("quantity_value", data.get("quantity", 1)))
|
||||
quantity_unit = normalize_quantity_unit(data.get("quantity_unit", "szt"))
|
||||
quantity = legacy_quantity_int(quantity_value)
|
||||
|
||||
list_obj = db.session.get(ShoppingList, list_id)
|
||||
if not list_obj:
|
||||
return
|
||||
|
||||
try:
|
||||
quantity = int(quantity)
|
||||
if quantity < 1:
|
||||
quantity = 1
|
||||
except:
|
||||
quantity = 1
|
||||
|
||||
existing_item = Item.query.filter(
|
||||
Item.list_id == list_id,
|
||||
func.lower(Item.name) == name.lower(),
|
||||
@@ -269,7 +266,14 @@ def handle_add_item(data):
|
||||
).first()
|
||||
|
||||
if existing_item:
|
||||
existing_item.quantity += quantity
|
||||
current_value = parse_quantity_value(getattr(existing_item, "quantity_value", None), existing_item.quantity or 1)
|
||||
current_unit = normalize_quantity_unit(getattr(existing_item, "quantity_unit", None))
|
||||
if current_unit == quantity_unit:
|
||||
existing_item.quantity_value = current_value + quantity_value
|
||||
else:
|
||||
existing_item.quantity_value = quantity_value
|
||||
existing_item.quantity_unit = quantity_unit
|
||||
existing_item.quantity = legacy_quantity_int(existing_item.quantity_value)
|
||||
db.session.commit()
|
||||
|
||||
emit(
|
||||
@@ -278,6 +282,9 @@ def handle_add_item(data):
|
||||
"item_id": existing_item.id,
|
||||
"new_name": existing_item.name,
|
||||
"new_quantity": existing_item.quantity,
|
||||
"quantity_value": existing_item.quantity_value,
|
||||
"quantity_unit": existing_item.quantity_unit,
|
||||
"quantity_label": quantity_label(existing_item),
|
||||
},
|
||||
to=str(list_id),
|
||||
)
|
||||
@@ -297,6 +304,8 @@ def handle_add_item(data):
|
||||
list_id=list_id,
|
||||
name=name,
|
||||
quantity=quantity,
|
||||
quantity_value=quantity_value,
|
||||
quantity_unit=quantity_unit,
|
||||
position=max_position + 1,
|
||||
added_by=user_id,
|
||||
)
|
||||
@@ -308,7 +317,7 @@ def handle_add_item(data):
|
||||
new_suggestion = SuggestedProduct(name=name)
|
||||
db.session.add(new_suggestion)
|
||||
|
||||
log_list_activity(list_id, 'item_added', item_name=new_item.name, actor=current_user if current_user.is_authenticated else None, actor_name=user_name, details=f'ilość: {new_item.quantity}')
|
||||
log_list_activity(list_id, 'item_added', item_name=new_item.name, actor=current_user if current_user.is_authenticated else None, actor_name=user_name, details=f'ilość: {quantity_label(new_item)}')
|
||||
db.session.commit()
|
||||
|
||||
emit(
|
||||
@@ -317,6 +326,9 @@ def handle_add_item(data):
|
||||
"id": new_item.id,
|
||||
"name": new_item.name,
|
||||
"quantity": new_item.quantity,
|
||||
"quantity_value": new_item.quantity_value,
|
||||
"quantity_unit": new_item.quantity_unit,
|
||||
"quantity_label": quantity_label(new_item),
|
||||
"added_by": user_name,
|
||||
"added_by_id": user_id,
|
||||
"owner_id": list_obj.owner_id,
|
||||
@@ -412,6 +424,9 @@ def handle_request_full_list(data):
|
||||
"id": item.id,
|
||||
"name": item.name,
|
||||
"quantity": item.quantity,
|
||||
"quantity_value": getattr(item, "quantity_value", None) or item.quantity or 1,
|
||||
"quantity_unit": normalize_quantity_unit(getattr(item, "quantity_unit", None)),
|
||||
"quantity_label": quantity_label(item),
|
||||
"purchased": item.purchased if not item.not_purchased else False,
|
||||
"not_purchased": item.not_purchased,
|
||||
"not_purchased_reason": item.not_purchased_reason,
|
||||
|
||||
@@ -1333,6 +1333,11 @@ input[type="checkbox"].form-check-input,
|
||||
max-width: 4.5rem;
|
||||
}
|
||||
|
||||
.shopping-qty-unit {
|
||||
flex: 0 0 5.2rem;
|
||||
max-width: 5.2rem;
|
||||
}
|
||||
|
||||
.shopping-compact-submit {
|
||||
flex: 0 0 auto;
|
||||
width: auto;
|
||||
@@ -1416,6 +1421,12 @@ input[type="checkbox"].form-check-input,
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.shopping-product-input-group > .shopping-qty-unit {
|
||||
flex: 0 0 5.2rem;
|
||||
max-width: 5.2rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.shopping-expense-input-group > .shopping-compact-submit,
|
||||
.shopping-product-input-group > .shopping-compact-submit {
|
||||
flex: 0 0 auto;
|
||||
@@ -1458,6 +1469,9 @@ input[type="checkbox"].form-check-input,
|
||||
.endpoint-list .shopping-product-input-group > .form-control,
|
||||
.endpoint-list_share .shopping-product-input-group > .form-control,
|
||||
.endpoint-shared_list .shopping-product-input-group > .form-control,
|
||||
.endpoint-list .shopping-product-input-group > .form-select,
|
||||
.endpoint-list_share .shopping-product-input-group > .form-select,
|
||||
.endpoint-shared_list .shopping-product-input-group > .form-select,
|
||||
.endpoint-list .shopping-expense-input-group > .form-control,
|
||||
.endpoint-list_share .shopping-expense-input-group > .form-control,
|
||||
.endpoint-shared_list .shopping-expense-input-group > .form-control,
|
||||
@@ -1484,7 +1498,10 @@ input[type="checkbox"].form-check-input,
|
||||
|
||||
.endpoint-list .shopping-product-input-group > .shopping-qty-input,
|
||||
.endpoint-list_share .shopping-product-input-group > .shopping-qty-input,
|
||||
.endpoint-shared_list .shopping-product-input-group > .shopping-qty-input {
|
||||
.endpoint-shared_list .shopping-product-input-group > .shopping-qty-input,
|
||||
.endpoint-list .shopping-product-input-group > .shopping-qty-unit,
|
||||
.endpoint-list_share .shopping-product-input-group > .shopping-qty-unit,
|
||||
.endpoint-shared_list .shopping-product-input-group > .shopping-qty-unit {
|
||||
border-radius: 0 !important;
|
||||
border-left-width: 0 !important;
|
||||
}
|
||||
@@ -1537,6 +1554,7 @@ input[type="checkbox"].form-check-input,
|
||||
}
|
||||
|
||||
.endpoint-view_list .shopping-product-input-group > .form-control,
|
||||
.endpoint-view_list .shopping-product-input-group > .form-select,
|
||||
.endpoint-view_list .shopping-expense-input-group > .form-control,
|
||||
.endpoint-view_list .shopping-product-input-group > .btn,
|
||||
.endpoint-view_list .shopping-expense-input-group > .btn {
|
||||
@@ -1551,7 +1569,8 @@ input[type="checkbox"].form-check-input,
|
||||
border-bottom-right-radius: 0 !important;
|
||||
}
|
||||
|
||||
.endpoint-view_list .shopping-product-input-group > .shopping-qty-input {
|
||||
.endpoint-view_list .shopping-product-input-group > .shopping-qty-input,
|
||||
.endpoint-view_list .shopping-product-input-group > .shopping-qty-unit {
|
||||
border-radius: 0 !important;
|
||||
border-left-width: 0 !important;
|
||||
}
|
||||
@@ -2092,7 +2111,11 @@ body:not(.sorting-active) .drag-handle {
|
||||
.endpoint-list .shopping-entry-card .shopping-product-input-group > .form-control,
|
||||
.endpoint-list_share .shopping-entry-card .shopping-product-input-group > .form-control,
|
||||
.endpoint-shared_list .shopping-entry-card .shopping-product-input-group > .form-control,
|
||||
.endpoint-view_list .shopping-entry-card .shopping-product-input-group > .form-control {
|
||||
.endpoint-view_list .shopping-entry-card .shopping-product-input-group > .form-control,
|
||||
.endpoint-list .shopping-entry-card .shopping-product-input-group > .form-select,
|
||||
.endpoint-list_share .shopping-entry-card .shopping-product-input-group > .form-select,
|
||||
.endpoint-shared_list .shopping-entry-card .shopping-product-input-group > .form-select,
|
||||
.endpoint-view_list .shopping-entry-card .shopping-product-input-group > .form-select {
|
||||
border-color: rgba(25, 135, 84, 0.55) !important;
|
||||
background: rgba(17, 24, 39, 0.95) !important;
|
||||
}
|
||||
@@ -2104,10 +2127,14 @@ body:not(.sorting-active) .drag-handle {
|
||||
color: rgba(255, 255, 255, 0.62);
|
||||
}
|
||||
|
||||
.endpoint-list .shopping-entry-card .shopping-product-input-group > .shopping-product-name-input:focus,
|
||||
.endpoint-list_share .shopping-entry-card .shopping-product-input-group > .shopping-product-name-input:focus,
|
||||
.endpoint-shared_list .shopping-entry-card .shopping-product-input-group > .shopping-product-name-input:focus,
|
||||
.endpoint-view_list .shopping-entry-card .shopping-product-input-group > .shopping-product-name-input:focus {
|
||||
.endpoint-list .shopping-entry-card .shopping-product-input-group > .form-control:focus,
|
||||
.endpoint-list_share .shopping-entry-card .shopping-product-input-group > .form-control:focus,
|
||||
.endpoint-shared_list .shopping-entry-card .shopping-product-input-group > .form-control:focus,
|
||||
.endpoint-view_list .shopping-entry-card .shopping-product-input-group > .form-control:focus,
|
||||
.endpoint-list .shopping-entry-card .shopping-product-input-group > .form-select:focus,
|
||||
.endpoint-list_share .shopping-entry-card .shopping-product-input-group > .form-select:focus,
|
||||
.endpoint-shared_list .shopping-entry-card .shopping-product-input-group > .form-select:focus,
|
||||
.endpoint-view_list .shopping-entry-card .shopping-product-input-group > .form-select:focus {
|
||||
box-shadow: inset 0 0 0 1px rgba(25, 135, 84, 0.25), 0 0 0 .2rem rgba(25, 135, 84, 0.18);
|
||||
}
|
||||
|
||||
@@ -2323,4 +2350,3 @@ body:not(.sorting-active) .drag-handle {
|
||||
#desktopItemMenu[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
|
||||
@@ -120,14 +120,20 @@
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-strong);
|
||||
text-align: right;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.receipt-disclosure__meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
margin-left: auto;
|
||||
flex-shrink: 0;
|
||||
flex: 0 1 auto;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.receipt-disclosure__count {
|
||||
|
||||
@@ -443,19 +443,27 @@
|
||||
|
||||
.shopping-product-input-group > .shopping-product-name-input,
|
||||
.shopping-expense-input-group > .shopping-expense-amount-input {
|
||||
flex: 0 0 60%;
|
||||
flex: 0 0 46%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.shopping-product-input-group > .shopping-qty-input {
|
||||
flex: 0 0 15%;
|
||||
max-width: 15%;
|
||||
flex: 0 0 16%;
|
||||
max-width: 16%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.shopping-product-input-group > .shopping-qty-unit {
|
||||
flex: 0 0 18%;
|
||||
max-width: 18% !important;
|
||||
min-width: 0;
|
||||
padding-left: .35rem;
|
||||
padding-right: .35rem;
|
||||
}
|
||||
|
||||
.shopping-product-input-group > .shopping-compact-submit {
|
||||
flex: 0 0 25%;
|
||||
width: 25%;
|
||||
flex: 0 0 20%;
|
||||
width: 20%;
|
||||
min-width: 0;
|
||||
padding-left: .55rem;
|
||||
padding-right: .55rem;
|
||||
@@ -489,16 +497,26 @@
|
||||
.endpoint-list .shopping-product-input-group > .shopping-product-name-input,
|
||||
.endpoint-list_share .shopping-product-input-group > .shopping-product-name-input,
|
||||
.endpoint-shared_list .shopping-product-input-group > .shopping-product-name-input {
|
||||
flex: 0 0 60% !important;
|
||||
max-width: 60% !important;
|
||||
flex: 0 0 46% !important;
|
||||
max-width: 46% !important;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.endpoint-list .shopping-product-input-group > .shopping-qty-input,
|
||||
.endpoint-list_share .shopping-product-input-group > .shopping-qty-input,
|
||||
.endpoint-shared_list .shopping-product-input-group > .shopping-qty-input {
|
||||
flex: 0 0 15% !important;
|
||||
max-width: 15% !important;
|
||||
flex: 0 0 16% !important;
|
||||
max-width: 16% !important;
|
||||
min-width: 0;
|
||||
padding-left: .35rem;
|
||||
padding-right: .35rem;
|
||||
}
|
||||
|
||||
.endpoint-list .shopping-product-input-group > .shopping-qty-unit,
|
||||
.endpoint-list_share .shopping-product-input-group > .shopping-qty-unit,
|
||||
.endpoint-shared_list .shopping-product-input-group > .shopping-qty-unit {
|
||||
flex: 0 0 18% !important;
|
||||
max-width: 18% !important;
|
||||
min-width: 0;
|
||||
padding-left: .35rem;
|
||||
padding-right: .35rem;
|
||||
@@ -507,8 +525,8 @@
|
||||
.endpoint-list .shopping-product-input-group > .shopping-compact-submit,
|
||||
.endpoint-list_share .shopping-product-input-group > .shopping-compact-submit,
|
||||
.endpoint-shared_list .shopping-product-input-group > .shopping-compact-submit {
|
||||
flex: 0 0 25% !important;
|
||||
width: 25% !important;
|
||||
flex: 0 0 20% !important;
|
||||
width: 20% !important;
|
||||
min-width: 0 !important;
|
||||
padding-left: .4rem;
|
||||
padding-right: .4rem;
|
||||
@@ -553,22 +571,30 @@
|
||||
|
||||
@media (max-width: 767.98px){
|
||||
.endpoint-view_list .shopping-product-input-group > .shopping-product-name-input {
|
||||
flex: 0 0 60% !important;
|
||||
max-width: 60% !important;
|
||||
flex: 0 0 46% !important;
|
||||
max-width: 46% !important;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.endpoint-view_list .shopping-product-input-group > .shopping-qty-input {
|
||||
flex: 0 0 15% !important;
|
||||
max-width: 15% !important;
|
||||
flex: 0 0 16% !important;
|
||||
max-width: 16% !important;
|
||||
min-width: 0;
|
||||
padding-left: .35rem;
|
||||
padding-right: .35rem;
|
||||
}
|
||||
|
||||
.endpoint-view_list .shopping-product-input-group > .shopping-qty-unit {
|
||||
flex: 0 0 18% !important;
|
||||
max-width: 18% !important;
|
||||
min-width: 0;
|
||||
padding-left: .35rem;
|
||||
padding-right: .35rem;
|
||||
}
|
||||
|
||||
.endpoint-view_list .shopping-product-input-group > .shopping-compact-submit {
|
||||
flex: 0 0 25% !important;
|
||||
width: 25% !important;
|
||||
flex: 0 0 20% !important;
|
||||
width: 20% !important;
|
||||
min-width: 0 !important;
|
||||
padding-left: .4rem;
|
||||
padding-right: .4rem;
|
||||
|
||||
@@ -74,24 +74,42 @@ function updateProgressBar() {
|
||||
if (percentValueEl) percentValueEl.textContent = percent;
|
||||
}
|
||||
|
||||
function parseQuantityInput(value, fallback = 1) {
|
||||
const parsed = parseFloat(String(value ?? fallback).replace(',', '.'));
|
||||
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
||||
}
|
||||
|
||||
function formatQuantityValue(value) {
|
||||
const parsed = parseQuantityInput(value, 1);
|
||||
return Number.isInteger(parsed) ? String(parsed) : parsed.toFixed(3).replace(/0+$/, '').replace(/\.$/, '');
|
||||
}
|
||||
|
||||
function quantityLabel(value, unit) {
|
||||
const safeUnit = unit || 'szt';
|
||||
const safeValue = formatQuantityValue(value);
|
||||
return safeUnit === 'szt' ? `x${safeValue}` : `${safeValue} ${safeUnit}`;
|
||||
}
|
||||
|
||||
function addItem(listId) {
|
||||
const name = document.getElementById('newItem').value;
|
||||
const quantityInput = document.getElementById('newQuantity');
|
||||
let quantity = 1;
|
||||
|
||||
if (quantityInput) {
|
||||
quantity = parseInt(quantityInput.value);
|
||||
if (isNaN(quantity) || quantity < 1) {
|
||||
quantity = 1;
|
||||
}
|
||||
}
|
||||
const unitInput = document.getElementById('newUnit');
|
||||
const quantityValue = parseQuantityInput(quantityInput?.value, 1);
|
||||
const quantityUnit = unitInput?.value || 'szt';
|
||||
|
||||
if (name.trim() === '') return;
|
||||
|
||||
socket.emit('add_item', { list_id: listId, name: name, quantity: quantity });
|
||||
socket.emit('add_item', {
|
||||
list_id: listId,
|
||||
name: name,
|
||||
quantity: Math.max(1, Math.round(quantityValue)),
|
||||
quantity_value: quantityValue,
|
||||
quantity_unit: quantityUnit
|
||||
});
|
||||
|
||||
document.getElementById('newItem').value = '';
|
||||
if (quantityInput) quantityInput.value = 1;
|
||||
if (unitInput) unitInput.value = 'szt';
|
||||
document.getElementById('newItem').focus();
|
||||
}
|
||||
|
||||
@@ -101,23 +119,26 @@ function deleteItem(id) {
|
||||
}
|
||||
}
|
||||
|
||||
function editItem(id, oldName, oldQuantity) {
|
||||
function editItem(id, oldName, oldQuantity, oldUnit = 'szt') {
|
||||
const finalName = String(oldName ?? '').trim();
|
||||
let newQuantity = parseInt(oldQuantity, 10);
|
||||
const newQuantity = parseQuantityInput(oldQuantity, 1);
|
||||
const newUnit = oldUnit || 'szt';
|
||||
|
||||
if (!finalName) {
|
||||
showToast('Nazwa produktu nie może być pusta.', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
if (isNaN(newQuantity) || newQuantity < 1) {
|
||||
newQuantity = 1;
|
||||
}
|
||||
|
||||
socket.emit('edit_item', { item_id: id, new_name: finalName, new_quantity: newQuantity });
|
||||
socket.emit('edit_item', {
|
||||
item_id: id,
|
||||
new_name: finalName,
|
||||
new_quantity: Math.max(1, Math.round(newQuantity)),
|
||||
quantity_value: newQuantity,
|
||||
quantity_unit: newUnit
|
||||
});
|
||||
}
|
||||
|
||||
function openEditItemModal(event, id, oldName, oldQuantity) {
|
||||
function openEditItemModal(event, id, oldName, oldQuantity, oldUnit = 'szt') {
|
||||
if (event && typeof event.stopPropagation === 'function') {
|
||||
event.stopPropagation();
|
||||
}
|
||||
@@ -126,17 +147,18 @@ function openEditItemModal(event, id, oldName, oldQuantity) {
|
||||
const idInput = document.getElementById('editItemId');
|
||||
const nameInput = document.getElementById('editItemName');
|
||||
const quantityInput = document.getElementById('editItemQuantity');
|
||||
const unitInput = document.getElementById('editItemUnit');
|
||||
|
||||
if (!modalEl || !idInput || !nameInput || !quantityInput || typeof bootstrap === 'undefined') {
|
||||
editItem(id, oldName, oldQuantity);
|
||||
if (!modalEl || !idInput || !nameInput || !quantityInput || !unitInput || typeof bootstrap === 'undefined') {
|
||||
editItem(id, oldName, oldQuantity, oldUnit);
|
||||
return;
|
||||
}
|
||||
|
||||
idInput.value = id;
|
||||
nameInput.value = String(oldName ?? '').trim();
|
||||
|
||||
const parsedQuantity = parseInt(oldQuantity, 10);
|
||||
quantityInput.value = !isNaN(parsedQuantity) && parsedQuantity > 0 ? parsedQuantity : 1;
|
||||
quantityInput.value = formatQuantityValue(oldQuantity);
|
||||
unitInput.value = oldUnit || 'szt';
|
||||
|
||||
const modal = bootstrap.Modal.getOrCreateInstance(modalEl);
|
||||
modal.show();
|
||||
@@ -389,8 +411,11 @@ function renderItem(item, isShare = window.IS_SHARE, optionsOrShowEditOnly = fal
|
||||
const safeName = escapeHtml(rawName);
|
||||
const nameForEdit = JSON.stringify(rawName);
|
||||
const quantity = Number.isInteger(item.quantity) ? item.quantity : parseInt(item.quantity, 10) || 1;
|
||||
const quantityBadge = quantity > 1
|
||||
? `<span class="badge rounded-pill bg-secondary">x${quantity}</span>`
|
||||
const quantityValue = parseQuantityInput(item.quantity_value ?? item.quantity, quantity);
|
||||
const quantityUnit = item.quantity_unit || 'szt';
|
||||
const quantityText = item.quantity_label || quantityLabel(quantityValue, quantityUnit);
|
||||
const quantityBadge = (quantityUnit !== 'szt' || quantityValue !== 1)
|
||||
? `<span class="badge rounded-pill bg-secondary">${escapeHtml(quantityText)}</span>`
|
||||
: '';
|
||||
|
||||
const canEditListItem = !isShare;
|
||||
@@ -424,7 +449,8 @@ function renderItem(item, isShare = window.IS_SHARE, optionsOrShowEditOnly = fal
|
||||
class="shopping-item-name text-white"
|
||||
data-item-id="${item.id}"
|
||||
data-item-name=${JSON.stringify(rawName)}
|
||||
data-item-quantity="${quantity}"
|
||||
data-item-quantity="${quantityValue}"
|
||||
data-item-unit="${quantityUnit}"
|
||||
${isArchived ? 'disabled aria-disabled="true"' : 'data-item-menu-trigger="true"'}>${safeName}</button>`
|
||||
: `<span id="name-${item.id}" class="shopping-item-name text-white">${safeName}</span>`;
|
||||
let actionButtons = '';
|
||||
@@ -436,7 +462,7 @@ function renderItem(item, isShare = window.IS_SHARE, optionsOrShowEditOnly = fal
|
||||
|
||||
actionButtons += `
|
||||
${dragHandleButton}
|
||||
<button type="button" class="${iconBtn}" ${isArchived ? 'disabled' : `onclick='openEditItemModal(event, ${item.id}, ${JSON.stringify(String(item.name || ''))}, ${quantity})'`}>✏️</button>
|
||||
<button type="button" class="${iconBtn}" ${isArchived ? 'disabled' : `onclick='openEditItemModal(event, ${item.id}, ${JSON.stringify(String(item.name || ''))}, ${quantityValue}, ${JSON.stringify(quantityUnit)})'`}>✏️</button>
|
||||
<button type="button" class="${iconBtn}" ${isArchived ? 'disabled' : `onclick="deleteItem(${item.id})"`}>🗑️</button>`;
|
||||
}
|
||||
|
||||
@@ -451,7 +477,7 @@ function renderItem(item, isShare = window.IS_SHARE, optionsOrShowEditOnly = fal
|
||||
if (temporaryShareUndo) {
|
||||
actionButtons += `
|
||||
<button type="button" class="${iconBtn} shopping-action-btn--countdown" disabled data-countdown-for="${item.id}">${countdownSeconds}s</button>
|
||||
<button type="button" class="${iconBtn}" ${isArchived ? 'disabled' : `onclick='openEditItemModal(event, ${item.id}, ${nameForEdit}, ${quantity})'`}>✏️</button>
|
||||
<button type="button" class="${iconBtn}" ${isArchived ? 'disabled' : `onclick='openEditItemModal(event, ${item.id}, ${nameForEdit}, ${quantityValue}, ${JSON.stringify(quantityUnit)})'`}>✏️</button>
|
||||
<button type="button" class="${iconBtn}" ${isArchived ? 'disabled' : `onclick="deleteItem(${item.id})"`}>🗑️</button>`;
|
||||
} else if (canShowShareActions || (!isShare && isOwner)) {
|
||||
actionButtons += `
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const editItemForm = document.getElementById('editItemForm');
|
||||
if (!editItemForm) return;
|
||||
|
||||
editItemForm.addEventListener('submit', function (event) {
|
||||
event.preventDefault();
|
||||
|
||||
const itemId = parseInt(document.getElementById('editItemId').value, 10);
|
||||
const itemName = document.getElementById('editItemName').value;
|
||||
const itemQuantity = document.getElementById('editItemQuantity').value;
|
||||
const itemUnit = document.getElementById('editItemUnit').value;
|
||||
|
||||
editItem(itemId, itemName, itemQuantity, itemUnit);
|
||||
|
||||
const modalEl = document.getElementById('editItemModal');
|
||||
const modal = bootstrap.Modal.getInstance(modalEl);
|
||||
if (modal) modal.hide();
|
||||
});
|
||||
});
|
||||
@@ -235,6 +235,9 @@ function setupList(listId, username) {
|
||||
id: itemId,
|
||||
name: data.new_name,
|
||||
quantity: data.new_quantity,
|
||||
quantity_value: data.quantity_value ?? data.new_quantity,
|
||||
quantity_unit: data.quantity_unit || 'szt',
|
||||
quantity_label: data.quantity_label || quantityLabel(data.quantity_value ?? data.new_quantity, data.quantity_unit || 'szt'),
|
||||
purchased: oldItem ? oldItem.classList.contains('bg-success') : !!cachedItem.purchased,
|
||||
not_purchased: oldItem ? oldItem.classList.contains('bg-warning') : !!cachedItem.not_purchased,
|
||||
not_purchased_reason: cachedItem.not_purchased_reason || '',
|
||||
@@ -252,7 +255,7 @@ function setupList(listId, username) {
|
||||
socket.emit('request_full_list', { list_id: window.LIST_ID });
|
||||
}
|
||||
|
||||
showToast(`Zaktualizowano produkt: ${data.new_name} (x${data.new_quantity})`, 'success');
|
||||
showToast(`Zaktualizowano produkt: ${data.new_name} (${data.quantity_label || quantityLabel(data.quantity_value ?? data.new_quantity, data.quantity_unit || 'szt')})`, 'success');
|
||||
|
||||
updateProgressBar();
|
||||
toggleEmptyPlaceholder();
|
||||
|
||||
@@ -9,6 +9,24 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
return str?.trim().toLowerCase() || '';
|
||||
}
|
||||
|
||||
function readQuantity(qty, fallback = 1) {
|
||||
const parsed = parseFloat(String(qty?.value ?? fallback).replace(',', '.'));
|
||||
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
||||
}
|
||||
|
||||
function makeUnitSelect() {
|
||||
const unit = document.createElement('select');
|
||||
unit.className = 'form-select form-select-sm bg-dark text-white border-secondary';
|
||||
unit.style.width = '78px';
|
||||
['szt', 'kg', 'g', 'l', 'ml', 'opak.'].forEach(value => {
|
||||
const option = document.createElement('option');
|
||||
option.value = value;
|
||||
option.textContent = value;
|
||||
unit.appendChild(option);
|
||||
});
|
||||
return unit;
|
||||
}
|
||||
|
||||
let sortMode = 'popularity';
|
||||
let limit = 25;
|
||||
let offset = 0;
|
||||
@@ -105,13 +123,54 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
}
|
||||
|
||||
function getAlreadyAddedProducts() {
|
||||
const set = new Set();
|
||||
document.querySelectorAll('#items li').forEach(li => {
|
||||
if (li.dataset.name) {
|
||||
set.add(normalize(li.dataset.name));
|
||||
}
|
||||
const map = new Map();
|
||||
document.querySelectorAll('#items li[id^="item-"]').forEach(li => {
|
||||
if (!li.dataset.name) return;
|
||||
const nameBtn = li.querySelector('[data-item-name]');
|
||||
const name = nameBtn?.dataset.itemName || li.dataset.name || '';
|
||||
map.set(normalize(li.dataset.name), {
|
||||
id: parseInt(li.id.replace('item-', ''), 10),
|
||||
name: name,
|
||||
quantityValue: readQuantity({ value: nameBtn?.dataset.itemQuantity || 1 }),
|
||||
unit: nameBtn?.dataset.itemUnit || 'szt'
|
||||
});
|
||||
});
|
||||
return set;
|
||||
return map;
|
||||
}
|
||||
|
||||
function appendQuantityControls(li, initialValue = 1, initialUnit = 'szt') {
|
||||
const qtyWrapper = document.createElement('div');
|
||||
qtyWrapper.className = 'd-flex align-items-center ms-2 quantity-controls';
|
||||
|
||||
const minusBtn = document.createElement('button');
|
||||
minusBtn.type = 'button';
|
||||
minusBtn.className = 'btn btn-outline-light btn-sm px-2';
|
||||
minusBtn.textContent = '−';
|
||||
|
||||
const qty = document.createElement('input');
|
||||
qty.type = 'number';
|
||||
qty.min = 0.001;
|
||||
qty.step = 0.001;
|
||||
qty.value = initialValue || 1;
|
||||
qty.className = 'form-control text-center p-1 rounded';
|
||||
qty.style.width = '62px';
|
||||
qty.style.margin = '0 2px';
|
||||
qty.title = 'Ilość / waga / objętość';
|
||||
|
||||
const plusBtn = document.createElement('button');
|
||||
plusBtn.type = 'button';
|
||||
plusBtn.className = 'btn btn-outline-light btn-sm px-2';
|
||||
plusBtn.textContent = '+';
|
||||
|
||||
const unit = makeUnitSelect();
|
||||
unit.value = initialUnit || 'szt';
|
||||
|
||||
minusBtn.onclick = () => { qty.value = Math.max(0.001, readQuantity(qty) - 1); };
|
||||
plusBtn.onclick = () => { qty.value = readQuantity(qty) + 1; };
|
||||
|
||||
qtyWrapper.append(minusBtn, qty, plusBtn, unit);
|
||||
li.appendChild(qtyWrapper);
|
||||
return { qty, unit, qtyWrapper };
|
||||
}
|
||||
|
||||
function renderProducts(products) {
|
||||
@@ -135,10 +194,12 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
if (addedProducts.has(normName)) {
|
||||
const nameSpan = document.createElement('span');
|
||||
nameSpan.textContent = name;
|
||||
nameSpan.style.flex = '1 1 auto';
|
||||
li.appendChild(nameSpan);
|
||||
li.classList.add('opacity-50');
|
||||
li.classList.add('opacity-50', 'border-success');
|
||||
|
||||
const badge = document.createElement('span');
|
||||
badge.className = 'badge bg-success ms-auto';
|
||||
badge.className = 'badge bg-success ms-2';
|
||||
badge.textContent = 'Dodano';
|
||||
li.appendChild(badge);
|
||||
} else {
|
||||
@@ -157,12 +218,15 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
|
||||
const qty = document.createElement('input');
|
||||
qty.type = 'number';
|
||||
qty.min = 1;
|
||||
qty.min = 0.001;
|
||||
qty.step = 0.001;
|
||||
qty.value = 1;
|
||||
qty.className = 'form-control text-center p-1 rounded';
|
||||
qty.style.width = '50px';
|
||||
qty.style.margin = '0 2px';
|
||||
qty.title = 'Ilość';
|
||||
qty.title = 'Ilość / waga / objętość';
|
||||
|
||||
const unit = makeUnitSelect();
|
||||
|
||||
const plusBtn = document.createElement('button');
|
||||
plusBtn.type = 'button';
|
||||
@@ -170,20 +234,26 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
plusBtn.textContent = '+';
|
||||
|
||||
minusBtn.onclick = () => {
|
||||
qty.value = Math.max(1, parseInt(qty.value) - 1);
|
||||
qty.value = Math.max(0.001, readQuantity(qty) - 1);
|
||||
};
|
||||
plusBtn.onclick = () => {
|
||||
qty.value = parseInt(qty.value) + 1;
|
||||
qty.value = readQuantity(qty) + 1;
|
||||
};
|
||||
|
||||
qtyWrapper.append(minusBtn, qty, plusBtn);
|
||||
qtyWrapper.append(minusBtn, qty, plusBtn, unit);
|
||||
|
||||
const btn = document.createElement('button');
|
||||
btn.className = 'btn btn-sm btn-primary ms-4';
|
||||
btn.textContent = '+';
|
||||
btn.onclick = () => {
|
||||
const quantity = parseInt(qty.value) || 1;
|
||||
socket.emit('add_item', { list_id: LIST_ID, name: name, quantity: quantity });
|
||||
const quantityValue = readQuantity(qty);
|
||||
socket.emit('add_item', {
|
||||
list_id: LIST_ID,
|
||||
name: name,
|
||||
quantity: Math.max(1, Math.round(quantityValue)),
|
||||
quantity_value: quantityValue,
|
||||
quantity_unit: unit.value
|
||||
});
|
||||
};
|
||||
|
||||
li.append(qtyWrapper, btn);
|
||||
@@ -266,12 +336,15 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
|
||||
const qty = document.createElement('input');
|
||||
qty.type = 'number';
|
||||
qty.min = 1;
|
||||
qty.min = 0.001;
|
||||
qty.step = 0.001;
|
||||
qty.value = 1;
|
||||
qty.className = 'form-control text-center p-1 rounded';
|
||||
qty.style.width = '50px';
|
||||
qty.style.margin = '0 2px';
|
||||
qty.title = 'Ilość';
|
||||
qty.title = 'Ilość / waga / objętość';
|
||||
|
||||
const unit = makeUnitSelect();
|
||||
|
||||
const plusBtn = document.createElement('button');
|
||||
plusBtn.type = 'button';
|
||||
@@ -279,24 +352,26 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
plusBtn.textContent = '+';
|
||||
|
||||
minusBtn.onclick = () => {
|
||||
qty.value = Math.max(1, parseInt(qty.value) - 1);
|
||||
qty.value = Math.max(0.001, readQuantity(qty) - 1);
|
||||
};
|
||||
plusBtn.onclick = () => {
|
||||
qty.value = parseInt(qty.value) + 1;
|
||||
qty.value = readQuantity(qty) + 1;
|
||||
};
|
||||
|
||||
qtyWrapper.append(minusBtn, qty, plusBtn);
|
||||
qtyWrapper.append(minusBtn, qty, plusBtn, unit);
|
||||
li.appendChild(qtyWrapper);
|
||||
|
||||
const addBtn = document.createElement('button');
|
||||
addBtn.className = 'btn btn-sm btn-primary ms-4';
|
||||
addBtn.textContent = '+';
|
||||
addBtn.onclick = () => {
|
||||
const quantity = parseInt(qty.value) || 1;
|
||||
const quantityValue = readQuantity(qty);
|
||||
socket.emit('add_item', {
|
||||
list_id: LIST_ID,
|
||||
name: data.name,
|
||||
quantity: quantity
|
||||
quantity: Math.max(1, Math.round(quantityValue)),
|
||||
quantity_value: quantityValue,
|
||||
quantity_unit: unit.value
|
||||
});
|
||||
};
|
||||
li.appendChild(addBtn);
|
||||
|
||||
@@ -18,13 +18,21 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
localStorage.setItem(storageKey, state ? "true" : "false");
|
||||
}
|
||||
|
||||
function getToggleLabel(shown) {
|
||||
const isMobile = window.matchMedia("(max-width: 575.98px)").matches;
|
||||
if (isMobile) {
|
||||
return shown ? "Ukryj" : "Pokaż";
|
||||
}
|
||||
return shown ? "Ukryj sekcję paragonów" : "Pokaż sekcję paragonów";
|
||||
}
|
||||
|
||||
function updateUI() {
|
||||
const shown = isShown();
|
||||
toggleEl.classList.toggle("is-open", shown);
|
||||
toggleEl.setAttribute("aria-expanded", shown ? "true" : "false");
|
||||
|
||||
if (titleEl) {
|
||||
titleEl.textContent = shown ? "Ukryj sekcję paragonów" : "Pokaż sekcję paragonów";
|
||||
titleEl.textContent = getToggleLabel(shown);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,5 +58,8 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
window.addEventListener("resize", updateUI);
|
||||
|
||||
updateUI();
|
||||
});
|
||||
|
||||
@@ -130,10 +130,13 @@
|
||||
class="shopping-item-name text-white"
|
||||
data-item-id="{{ item.id }}"
|
||||
data-item-name={{ item.name|tojson }}
|
||||
data-item-quantity="{{ item.quantity or 1 }}"
|
||||
data-item-quantity="{{ item.quantity_value or item.quantity or 1 }}"
|
||||
data-item-unit="{{ item.quantity_unit or 'szt' }}"
|
||||
{% if not list.is_archived %}data-item-menu-trigger="true"{% else %}disabled aria-disabled="true"{% endif %}>{{ item.name }}</button>
|
||||
{% if item.quantity and item.quantity > 1 %}
|
||||
<span class="badge rounded-pill bg-secondary">x{{ item.quantity }}</span>
|
||||
{% set qty_value = item.quantity_value or item.quantity or 1 %}
|
||||
{% set qty_unit = item.quantity_unit or 'szt' %}
|
||||
{% if qty_unit != 'szt' or qty_value != 1 %}
|
||||
<span class="badge rounded-pill bg-secondary">{% if qty_unit == 'szt' %}x{{ ('%g' % qty_value) }}{% else %}{{ ('%g' % qty_value) }} {{ qty_unit }}{% endif %}</span>
|
||||
{% endif %}
|
||||
{% set info_parts = [] %}
|
||||
{% if item.note %}{% set _ = info_parts.append('<span class="text-danger">[ <b>' ~ item.note ~ '</b> ]</span>') %}{% endif %}
|
||||
@@ -146,7 +149,8 @@
|
||||
<div class="list-item-actions shopping-item-actions" role="group">
|
||||
{% if not is_share %}
|
||||
<button type="button" class="btn btn-outline-light btn-sm shopping-action-btn" {% if list.is_archived %}disabled{% else
|
||||
%}onclick='openEditItemModal(event, {{ item.id }}, {{ item.name|tojson }}, {{ item.quantity or 1 }})' {% endif %}>✏️</button>
|
||||
%}onclick='openEditItemModal(event, {{ item.id }}, {{ item.name|tojson }}, {{ item.quantity_value or item.quantity or 1 }}, {{ (item.quantity_unit or "szt")|tojson }})' {% endif %}>✏️</button>
|
||||
|
||||
<button type="button" class="btn btn-outline-light btn-sm shopping-action-btn" {% if list.is_archived %}disabled{% else
|
||||
%}onclick="deleteItem({{ item.id }})" {% endif %}>🗑️</button>
|
||||
{% endif %}
|
||||
@@ -192,8 +196,18 @@
|
||||
<input type="text" id="editItemName" class="form-control bg-dark text-white border-secondary" maxlength="255" required>
|
||||
</div>
|
||||
<div>
|
||||
<label for="editItemQuantity" class="form-label">Ilość</label>
|
||||
<input type="number" id="editItemQuantity" class="form-control bg-dark text-white border-secondary" min="1" step="1" required>
|
||||
<label for="editItemQuantity" class="form-label">Ilość / waga / objętość</label>
|
||||
<div class="input-group">
|
||||
<input type="number" id="editItemQuantity" class="form-control bg-dark text-white border-secondary" min="0.001" step="0.001" required>
|
||||
<select id="editItemUnit" class="form-select bg-dark text-white border-secondary" style="max-width: 110px;">
|
||||
<option value="szt">szt</option>
|
||||
<option value="kg">kg</option>
|
||||
<option value="g">g</option>
|
||||
<option value="l">l</option>
|
||||
<option value="ml">ml</option>
|
||||
<option value="opak.">opak.</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -221,7 +235,18 @@
|
||||
|
||||
<input type="number" id="newQuantity" name="quantity"
|
||||
class="form-control bg-dark text-white border-secondary shopping-qty-input"
|
||||
placeholder="Ilość" min="1" value="1">
|
||||
placeholder="Ilość" min="0.001" step="0.001" value="1">
|
||||
|
||||
<select id="newUnit" name="quantity_unit"
|
||||
class="form-select bg-dark text-white border-secondary shopping-qty-unit"
|
||||
style="max-width: 105px;">
|
||||
<option value="szt">szt</option>
|
||||
<option value="kg">kg</option>
|
||||
<option value="g">g</option>
|
||||
<option value="l">l</option>
|
||||
<option value="ml">ml</option>
|
||||
<option value="opak.">opak.</option>
|
||||
</select>
|
||||
|
||||
<button type="button"
|
||||
class="btn btn-outline-success share-submit-btn shopping-compact-submit"
|
||||
@@ -555,28 +580,8 @@
|
||||
<script src="{{ static_asset_url('static_bp.serve_js', 'desktop_item_menu.js') }}"></script>
|
||||
<script>
|
||||
setupList({{ list.id }}, '{{ current_user.username if current_user.is_authenticated else 'Gość' }}');
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const editItemForm = document.getElementById('editItemForm');
|
||||
if (!editItemForm) return;
|
||||
|
||||
editItemForm.addEventListener('submit', function (event) {
|
||||
event.preventDefault();
|
||||
|
||||
const itemId = parseInt(document.getElementById('editItemId').value, 10);
|
||||
const itemName = document.getElementById('editItemName').value;
|
||||
const itemQuantity = document.getElementById('editItemQuantity').value;
|
||||
|
||||
editItem(itemId, itemName, itemQuantity);
|
||||
|
||||
const modalEl = document.getElementById('editItemModal');
|
||||
const modal = bootstrap.Modal.getInstance(modalEl);
|
||||
if (modal) {
|
||||
modal.hide();
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<script src="{{ static_asset_url('static_bp.serve_js', 'list_item_form.js') }}"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% endblock %}
|
||||
@@ -53,8 +53,10 @@
|
||||
<div class="shopping-item-head">
|
||||
<div class="shopping-item-text">
|
||||
<span id="name-{{ item.id }}" class="shopping-item-name text-white">{{ item.name }}</span>
|
||||
{% if item.quantity and item.quantity > 1 %}
|
||||
<span class="badge rounded-pill bg-secondary">x{{ item.quantity }}</span>
|
||||
{% set qty_value = item.quantity_value or item.quantity or 1 %}
|
||||
{% set qty_unit = item.quantity_unit or 'szt' %}
|
||||
{% if qty_unit != 'szt' or qty_value != 1 %}
|
||||
<span class="badge rounded-pill bg-secondary">{% if qty_unit == 'szt' %}x{{ ('%g' % qty_value) }}{% else %}{{ ('%g' % qty_value) }} {{ qty_unit }}{% endif %}</span>
|
||||
{% endif %}
|
||||
{% set info_parts = [] %}
|
||||
{% if item.note %}{% set _ = info_parts.append('<span class="text-danger">[ <b>' ~ item.note ~ '</b> ]</span>') %}{% endif %}
|
||||
@@ -101,7 +103,17 @@
|
||||
<input id="newItem" class="form-control bg-dark text-white border-secondary shopping-product-name-input" placeholder="Dodaj produkt" {% if
|
||||
not current_user.is_authenticated %}disabled{% endif %}>
|
||||
<input id="newQuantity" type="number" class="form-control bg-dark text-white border-secondary shopping-qty-input" placeholder="Ilość"
|
||||
min="1" value="1" {% if not current_user.is_authenticated %}disabled{% endif %}>
|
||||
min="0.001" step="0.001" value="1" {% if not current_user.is_authenticated %}disabled{% endif %}>
|
||||
<select id="newUnit" name="quantity_unit"
|
||||
class="form-select bg-dark text-white border-secondary shopping-qty-unit"
|
||||
style="max-width: 105px;" {% if not current_user.is_authenticated %}disabled{% endif %}>
|
||||
<option value="szt">szt</option>
|
||||
<option value="kg">kg</option>
|
||||
<option value="g">g</option>
|
||||
<option value="l">l</option>
|
||||
<option value="ml">ml</option>
|
||||
<option value="opak.">opak.</option>
|
||||
</select>
|
||||
<button onclick="addItem({{ list.id }})" class="btn btn-outline-success share-submit-btn shopping-compact-submit" {% if not
|
||||
current_user.is_authenticated %}disabled{% endif %}><span class="shopping-btn-icon" aria-hidden="true">➕</span><span class="shopping-btn-label">Dodaj</span></button>
|
||||
</div>
|
||||
@@ -144,9 +156,9 @@
|
||||
<span class="receipt-disclosure__icon" aria-hidden="true">🧾</span>
|
||||
<span class="receipt-disclosure__text">
|
||||
<span class="receipt-disclosure__eyebrow">Strefa paragonów</span>
|
||||
<span class="receipt-disclosure__title">Pokaż sekcję paragonów</span>
|
||||
</span>
|
||||
<span class="receipt-disclosure__meta">
|
||||
<span class="receipt-disclosure__title">Pokaż sekcję paragonów</span>
|
||||
<span class="receipt-disclosure__count">{{ receipts|length }}</span>
|
||||
<span class="receipt-disclosure__chevron" aria-hidden="true">⌄</span>
|
||||
</span>
|
||||
@@ -295,6 +307,47 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="modal fade" id="editItemModal" tabindex="-1" aria-labelledby="editItemModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content bg-dark text-white">
|
||||
<form id="editItemForm">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="editItemModalLabel">Edytuj produkt</h5>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Zamknij"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<input type="hidden" id="editItemId">
|
||||
<div class="mb-3">
|
||||
<label for="editItemName" class="form-label">Nazwa produktu</label>
|
||||
<input type="text" id="editItemName" class="form-control bg-dark text-white border-secondary" maxlength="255" required>
|
||||
</div>
|
||||
<div>
|
||||
<label for="editItemQuantity" class="form-label">Ilość / waga / objętość</label>
|
||||
<div class="input-group">
|
||||
<input type="number" id="editItemQuantity" class="form-control bg-dark text-white border-secondary" min="0.001" step="0.001" required>
|
||||
<select id="editItemUnit" class="form-select bg-dark text-white border-secondary" style="max-width: 110px;">
|
||||
<option value="szt">szt</option>
|
||||
<option value="kg">kg</option>
|
||||
<option value="g">g</option>
|
||||
<option value="l">l</option>
|
||||
<option value="ml">ml</option>
|
||||
<option value="opak.">opak.</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer justify-content-end">
|
||||
<div class="btn-group" role="group">
|
||||
<button type="button" class="btn btn-sm btn-outline-light" data-bs-dismiss="modal">❌ Anuluj</button>
|
||||
<button type="submit" class="btn btn-sm btn-outline-light"><span class="shopping-btn-icon" aria-hidden="true">💾</span><span class="shopping-btn-label">Zapisz</span></button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal notatki -->
|
||||
<div class="modal fade" id="noteModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg modal-dialog-scrollable">
|
||||
@@ -341,6 +394,7 @@
|
||||
<script>
|
||||
setupList({{ list.id }}, '{{ current_user.username if current_user.is_authenticated else 'Gość' }}');
|
||||
</script>
|
||||
<script src="{{ static_asset_url('static_bp.serve_js', 'list_item_form.js') }}"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user