Compare commits
6 Commits
v1.1.1
...
ca3ef5f6d7
| Author | SHA1 | Date | |
|---|---|---|---|
| ca3ef5f6d7 | |||
| a3d1e8767f | |||
| a1dcf36d1f | |||
| 6a89f20384 | |||
| 172b46ad07 | |||
| 4c9d665ae2 |
+7
-1
@@ -195,4 +195,10 @@ UPLOADS_CACHE_CONTROL="max-age=3600, immutable"
|
||||
# UWAGA: wielkość liter w nazwach jest zachowywana, ale porównywanie odbywa się
|
||||
# bez rozróżniania wielkości liter (case-insensitive).
|
||||
# Domyślnie: poniższa lista
|
||||
DEFAULT_CATEGORIES="Spożywcze,Budowlane,Zabawki,Chemia,Inne,Elektronika,Odzież i obuwie,Artykuły biurowe,Kosmetyki i higiena,Motoryzacja,Ogród i rośliny,Zwierzęta,Sprzęt sportowy,Książki i prasa,Narzędzia i majsterkowanie,RTV / AGD,Apteka i suplementy,Artykuły dekoracyjne,Gry i hobby,Usługi,Pieczywo"
|
||||
DEFAULT_CATEGORIES="Spożywcze,Budowlane,Zabawki,Chemia,Inne,Elektronika,Odzież i obuwie,Artykuły biurowe,Kosmetyki i higiena,Motoryzacja,Ogród i rośliny,Zwierzęta,Sprzęt sportowy,Książki i prasa,Narzędzia i majsterkowanie,RTV / AGD,Apteka i suplementy,Artykuły dekoracyjne,Gry i hobby,Usługi,Pieczywo"
|
||||
|
||||
# Waluta używana w całej aplikacji (kwoty, paragony, analizy)
|
||||
# Użyj kodu ISO 4217 (np. PLN, EUR, USD, GBP)
|
||||
# Domyślnie: PLN (jeśli zmienna nie jest ustawiona)
|
||||
|
||||
CURRENCY_CODE=PLN
|
||||
+2
-1
@@ -10,4 +10,5 @@ db/pgsql/*
|
||||
db/shopping.db
|
||||
*.swp
|
||||
version.txt
|
||||
deploy/varnish/default.vcl
|
||||
deploy/varnish/default.vcl
|
||||
*.zip
|
||||
@@ -91,6 +91,8 @@ class Config:
|
||||
DEBUG_MODE = env_bool("DEBUG_MODE", True)
|
||||
DISABLE_ROBOTS = env_bool("DISABLE_ROBOTS", False)
|
||||
|
||||
CURRENCY_CODE = env_str("CURRENCY_CODE", "PLN").strip().upper() or "PLN"
|
||||
|
||||
JS_CACHE_CONTROL = env_str("JS_CACHE_CONTROL", "no-cache")
|
||||
CSS_CACHE_CONTROL = env_str("CSS_CACHE_CONTROL", "no-cache")
|
||||
LIB_JS_CACHE_CONTROL = env_str("LIB_JS_CACHE_CONTROL", "max-age=604800")
|
||||
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
#!/usr/bin/env python3
|
||||
import os
|
||||
import sys
|
||||
import zipfile
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def run_git_command(args, repo_path: Path) -> bytes:
|
||||
result = subprocess.run(
|
||||
["git", *args],
|
||||
cwd=repo_path,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
check=True,
|
||||
)
|
||||
return result.stdout
|
||||
|
||||
|
||||
def get_files_to_archive(repo_path: Path) -> list[str]:
|
||||
output = run_git_command(
|
||||
["ls-files", "--cached", "--others", "--exclude-standard", "-z"],
|
||||
repo_path,
|
||||
)
|
||||
files = output.decode("utf-8", errors="surrogateescape").split("\0")
|
||||
return [f for f in files if f]
|
||||
|
||||
|
||||
def make_zip(repo_path: Path, output_zip: Path) -> None:
|
||||
files = get_files_to_archive(repo_path)
|
||||
|
||||
output_zip = output_zip.resolve()
|
||||
if output_zip.exists():
|
||||
output_zip.unlink()
|
||||
|
||||
with zipfile.ZipFile(output_zip, "w", compression=zipfile.ZIP_DEFLATED) as zf:
|
||||
for rel_path in files:
|
||||
abs_path = repo_path / rel_path
|
||||
|
||||
if not abs_path.exists():
|
||||
continue
|
||||
|
||||
if abs_path.resolve() == output_zip:
|
||||
continue
|
||||
|
||||
zf.write(abs_path, arcname=rel_path)
|
||||
|
||||
print(f"Utworzono archiwum: {output_zip}")
|
||||
print(f"Added files: {len(files)}")
|
||||
|
||||
|
||||
def main():
|
||||
repo_path = Path.cwd()
|
||||
|
||||
if len(sys.argv) > 1:
|
||||
output_zip = Path(sys.argv[1])
|
||||
else:
|
||||
output_zip = repo_path / f"{repo_path.name}.zip"
|
||||
|
||||
try:
|
||||
run_git_command(["rev-parse", "--show-toplevel"], repo_path)
|
||||
except subprocess.CalledProcessError:
|
||||
print("Error: this directory is not a Git repository.", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
make_zip(repo_path, output_zip)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -2,6 +2,25 @@ from .deps import *
|
||||
from .app_setup import *
|
||||
from .models import *
|
||||
|
||||
|
||||
def get_currency_code() -> str:
|
||||
code = str(app.config.get("CURRENCY_CODE", "PLN") or "PLN").strip().upper()
|
||||
return code or "PLN"
|
||||
|
||||
|
||||
def format_currency(amount, include_code: bool = True) -> str:
|
||||
try:
|
||||
normalized = float(amount or 0)
|
||||
except (TypeError, ValueError):
|
||||
normalized = 0.0
|
||||
formatted = f"{normalized:.2f}"
|
||||
return f"{formatted} {get_currency_code()}" if include_code else formatted
|
||||
|
||||
|
||||
def currency_placeholder(prefix: str = "Kwota") -> str:
|
||||
return f"{prefix} ({get_currency_code()})"
|
||||
|
||||
|
||||
def get_setting(key: str, default: str | None = None) -> str | None:
|
||||
s = db.session.get(AppSetting, key)
|
||||
return s.value if s else default
|
||||
@@ -230,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,
|
||||
@@ -346,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,
|
||||
@@ -555,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,
|
||||
))
|
||||
@@ -587,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,
|
||||
@@ -622,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)
|
||||
@@ -645,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)
|
||||
|
||||
|
||||
+37
-22
@@ -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,
|
||||
@@ -453,7 +468,7 @@ def handle_add_expense(data):
|
||||
)
|
||||
|
||||
db.session.add(new_expense)
|
||||
log_list_activity(list_id, 'expense_added', item_name=None, actor=current_user if current_user.is_authenticated else None, actor_name=current_user.username if current_user.is_authenticated else 'Gość', details=f'kwota: {float(amount):.2f} PLN')
|
||||
log_list_activity(list_id, 'expense_added', item_name=None, actor=current_user if current_user.is_authenticated else None, actor_name=current_user.username if current_user.is_authenticated else 'Gość', details=f'kwota: {format_currency(amount)}')
|
||||
db.session.commit()
|
||||
|
||||
total = (
|
||||
|
||||
@@ -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;
|
||||
@@ -1382,6 +1387,14 @@ input[type="checkbox"].form-check-input,
|
||||
min-width: 0;
|
||||
overflow-wrap: break-word;
|
||||
word-break: normal;
|
||||
appearance: none;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
text-align: left;
|
||||
font: inherit;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.shopping-item-text .info-line {
|
||||
@@ -1408,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;
|
||||
@@ -1450,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,
|
||||
@@ -1476,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;
|
||||
}
|
||||
@@ -1529,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 {
|
||||
@@ -1543,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;
|
||||
}
|
||||
@@ -2084,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;
|
||||
}
|
||||
@@ -2096,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);
|
||||
}
|
||||
|
||||
@@ -2284,3 +2319,34 @@ body:not(.sorting-active) .drag-handle {
|
||||
color: rgba(255,255,255,.66);
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
|
||||
.endpoint-view_list .shopping-item-name[data-item-menu-trigger],
|
||||
.endpoint-list .shopping-item-name[data-item-menu-trigger] {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.endpoint-view_list .shopping-item-name[data-item-menu-trigger]:hover,
|
||||
.endpoint-view_list .shopping-item-name[data-item-menu-trigger]:focus-visible,
|
||||
.endpoint-list .shopping-item-name[data-item-menu-trigger]:hover,
|
||||
.endpoint-list .shopping-item-name[data-item-menu-trigger]:focus-visible {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
#desktopItemMenu {
|
||||
position: fixed;
|
||||
z-index: 1200;
|
||||
min-width: 10rem;
|
||||
display: grid;
|
||||
gap: .35rem;
|
||||
padding: .45rem;
|
||||
border: 1px solid rgba(255, 255, 255, .12);
|
||||
border-radius: .9rem;
|
||||
background: rgba(18, 20, 24, .96);
|
||||
box-shadow: 0 16px 38px rgba(0, 0, 0, .34);
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
#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;
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
(function () {
|
||||
const DESKTOP_QUERY = '(min-width: 992px) and (pointer: fine)';
|
||||
|
||||
function isDesktopOwnerList() {
|
||||
return window.matchMedia(DESKTOP_QUERY).matches
|
||||
&& !window.IS_SHARE
|
||||
&& (
|
||||
document.body.classList.contains('endpoint-view_list')
|
||||
|| document.body.classList.contains('endpoint-list')
|
||||
);
|
||||
}
|
||||
|
||||
function getMenu() {
|
||||
return document.getElementById('desktopItemMenu');
|
||||
}
|
||||
|
||||
function hideDesktopItemMenu() {
|
||||
const menu = getMenu();
|
||||
if (!menu) return;
|
||||
menu.hidden = true;
|
||||
delete menu.dataset.itemId;
|
||||
delete menu.dataset.itemName;
|
||||
delete menu.dataset.itemQuantity;
|
||||
}
|
||||
|
||||
function positionMenu(menu, clickX, clickY) {
|
||||
const gap = 14;
|
||||
menu.style.left = '0px';
|
||||
menu.style.top = '0px';
|
||||
menu.hidden = false;
|
||||
|
||||
const rect = menu.getBoundingClientRect();
|
||||
const viewportWidth = window.innerWidth;
|
||||
const viewportHeight = window.innerHeight;
|
||||
|
||||
let left = clickX - (rect.width / 2);
|
||||
let top = clickY - rect.height - gap;
|
||||
|
||||
if (top < 12) {
|
||||
top = Math.min(viewportHeight - rect.height - 12, clickY + gap);
|
||||
}
|
||||
|
||||
left = Math.max(12, Math.min(left, viewportWidth - rect.width - 12));
|
||||
top = Math.max(12, Math.min(top, viewportHeight - rect.height - 12));
|
||||
|
||||
menu.style.left = `${left}px`;
|
||||
menu.style.top = `${top}px`;
|
||||
}
|
||||
|
||||
function showDesktopItemMenu(trigger, event) {
|
||||
const menu = getMenu();
|
||||
if (!menu) return;
|
||||
|
||||
menu.dataset.itemId = trigger.dataset.itemId || '';
|
||||
menu.dataset.itemName = trigger.dataset.itemName || '';
|
||||
menu.dataset.itemQuantity = trigger.dataset.itemQuantity || '1';
|
||||
|
||||
let clickX = event.clientX || 0;
|
||||
let clickY = event.clientY || 0;
|
||||
|
||||
if (!clickX && !clickY) {
|
||||
const rect = trigger.getBoundingClientRect();
|
||||
clickX = rect.left + (rect.width / 2);
|
||||
clickY = rect.top;
|
||||
}
|
||||
|
||||
positionMenu(menu, clickX, clickY);
|
||||
}
|
||||
|
||||
document.addEventListener('click', function (event) {
|
||||
const menu = getMenu();
|
||||
const trigger = event.target.closest('[data-item-menu-trigger="true"]');
|
||||
|
||||
if (trigger && isDesktopOwnerList() && !trigger.disabled) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
showDesktopItemMenu(trigger, event);
|
||||
return;
|
||||
}
|
||||
|
||||
if (menu && !menu.hidden && !event.target.closest('#desktopItemMenu')) {
|
||||
hideDesktopItemMenu();
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('keydown', function (event) {
|
||||
if (event.key === 'Escape') {
|
||||
hideDesktopItemMenu();
|
||||
}
|
||||
});
|
||||
|
||||
['scroll', 'resize'].forEach(function (eventName) {
|
||||
window.addEventListener(eventName, hideDesktopItemMenu, true);
|
||||
});
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const menu = getMenu();
|
||||
if (!menu) return;
|
||||
|
||||
menu.addEventListener('click', function (event) {
|
||||
const actionButton = event.target.closest('[data-menu-action]');
|
||||
if (!actionButton) return;
|
||||
|
||||
const itemId = parseInt(menu.dataset.itemId || '', 10);
|
||||
const itemName = menu.dataset.itemName || '';
|
||||
const itemQuantity = parseInt(menu.dataset.itemQuantity || '1', 10) || 1;
|
||||
|
||||
if (!itemId) {
|
||||
hideDesktopItemMenu();
|
||||
return;
|
||||
}
|
||||
|
||||
if (actionButton.dataset.menuAction === 'edit') {
|
||||
openEditItemModal(event, itemId, itemName, itemQuantity);
|
||||
}
|
||||
|
||||
if (actionButton.dataset.menuAction === 'delete') {
|
||||
deleteItem(itemId);
|
||||
}
|
||||
|
||||
hideDesktopItemMenu();
|
||||
});
|
||||
});
|
||||
|
||||
window.hideDesktopItemMenu = hideDesktopItemMenu;
|
||||
})();
|
||||
@@ -123,7 +123,7 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
data: {
|
||||
labels: data.labels || [],
|
||||
datasets: [{
|
||||
label: "Suma wydatków [PLN]",
|
||||
label: `Suma wydatków [${getCurrencyCode()}]`,
|
||||
data: data.expenses || [],
|
||||
}],
|
||||
},
|
||||
|
||||
@@ -28,7 +28,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
total += parseFloat(cb.dataset.amount);
|
||||
}
|
||||
});
|
||||
totalEl.textContent = total.toFixed(2) + ' PLN';
|
||||
totalEl.textContent = formatCurrencyAmount(total);
|
||||
}
|
||||
|
||||
function getISOWeek(date) {
|
||||
|
||||
@@ -1,3 +1,19 @@
|
||||
function getCurrencyCode() {
|
||||
return window.CURRENCY_CODE || 'PLN';
|
||||
}
|
||||
|
||||
function formatCurrencyAmount(amount, options = {}) {
|
||||
const includeCode = options.includeCode !== false;
|
||||
const numeric = Number(amount || 0);
|
||||
const safe = Number.isFinite(numeric) ? numeric : 0;
|
||||
const formatted = safe.toFixed(2);
|
||||
return includeCode ? `${formatted} ${getCurrencyCode()}` : formatted;
|
||||
}
|
||||
|
||||
function currencyLabel(prefix = 'Kwota') {
|
||||
return `${prefix} (${getCurrencyCode()})`;
|
||||
}
|
||||
|
||||
function updateItemState(itemId, isChecked) {
|
||||
const checkbox = document.querySelector(`#item-${itemId} input[type='checkbox']`);
|
||||
if (checkbox) {
|
||||
@@ -58,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();
|
||||
}
|
||||
|
||||
@@ -85,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();
|
||||
}
|
||||
@@ -110,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();
|
||||
@@ -369,11 +407,15 @@ function renderItem(item, isShare = window.IS_SHARE, optionsOrShowEditOnly = fal
|
||||
|
||||
const isOwner = window.IS_OWNER === true || window.IS_OWNER === 'true';
|
||||
const isArchived = window.IS_ARCHIVED === true || window.IS_ARCHIVED === 'true';
|
||||
const safeName = escapeHtml(item.name || '');
|
||||
const nameForEdit = JSON.stringify(String(item.name || ''));
|
||||
const rawName = String(item.name || '');
|
||||
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;
|
||||
@@ -401,6 +443,16 @@ function renderItem(item, isShare = window.IS_SHARE, optionsOrShowEditOnly = fal
|
||||
|
||||
const iconBtn = 'btn btn-outline-light btn-sm shopping-action-btn';
|
||||
const wideBtn = 'btn btn-outline-light btn-sm shopping-action-btn shopping-action-btn--wide';
|
||||
const itemNameHtml = canEditListItem
|
||||
? `<button type="button"
|
||||
id="name-${item.id}"
|
||||
class="shopping-item-name text-white"
|
||||
data-item-id="${item.id}"
|
||||
data-item-name=${JSON.stringify(rawName)}
|
||||
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 = '';
|
||||
|
||||
if (canEditListItem) {
|
||||
@@ -410,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>`;
|
||||
}
|
||||
|
||||
@@ -425,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 += `
|
||||
@@ -438,7 +490,7 @@ function renderItem(item, isShare = window.IS_SHARE, optionsOrShowEditOnly = fal
|
||||
<div class="shopping-item-content">
|
||||
<div class="shopping-item-head">
|
||||
<div class="shopping-item-text">
|
||||
<span id="name-${item.id}" class="shopping-item-name text-white">${safeName}</span>
|
||||
${itemNameHtml}
|
||||
${quantityBadge}
|
||||
${infoHtml}
|
||||
</div>
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -113,7 +113,7 @@ function setupList(listId, username) {
|
||||
socket.on('expense_added', data => {
|
||||
const badgeEl = document.getElementById('total-expense1');
|
||||
if (badgeEl) {
|
||||
badgeEl.innerHTML = `💸 ${data.total.toFixed(2)} PLN`;
|
||||
badgeEl.innerHTML = `💸 ${formatCurrencyAmount(data.total)}`;
|
||||
badgeEl.classList.remove('bg-secondary');
|
||||
badgeEl.classList.add('bg-success');
|
||||
badgeEl.style.display = '';
|
||||
@@ -121,10 +121,10 @@ function setupList(listId, username) {
|
||||
|
||||
const summaryEl = document.getElementById('total-expense2');
|
||||
if (summaryEl) {
|
||||
summaryEl.innerHTML = `<b>💸 Łącznie wydano:</b> ${data.total.toFixed(2)} PLN`;
|
||||
summaryEl.innerHTML = `<b>💸 Łącznie wydano:</b> ${formatCurrencyAmount(data.total)}`;
|
||||
}
|
||||
|
||||
showToast(`Dodano wydatek: ${data.amount.toFixed(2)} PLN`, 'info');
|
||||
showToast(`Dodano wydatek: ${formatCurrencyAmount(data.amount)}`, 'info');
|
||||
});
|
||||
|
||||
|
||||
@@ -139,6 +139,13 @@ function setupList(listId, username) {
|
||||
note: ''
|
||||
};
|
||||
|
||||
// Note: store newly added items locally so later edits do not depend on a full page refresh.
|
||||
if (Array.isArray(window.currentItems)) {
|
||||
window.currentItems.push(item);
|
||||
} else {
|
||||
window.currentItems = [item];
|
||||
}
|
||||
|
||||
const isOwnFreshShareItem = Boolean(
|
||||
window.IS_SHARE &&
|
||||
data.added_by &&
|
||||
@@ -216,22 +223,43 @@ function setupList(listId, username) {
|
||||
});
|
||||
|
||||
socket.on('item_edited', data => {
|
||||
const idx = window.currentItems.findIndex(i => i.id === data.item_id);
|
||||
if (idx !== -1) {
|
||||
window.currentItems[idx].name = data.new_name;
|
||||
window.currentItems[idx].quantity = data.new_quantity;
|
||||
const itemId = Number(data.item_id);
|
||||
const oldItem = document.getElementById(`item-${itemId}`);
|
||||
const currentItems = Array.isArray(window.currentItems) ? window.currentItems : [];
|
||||
const idx = currentItems.findIndex(item => Number(item.id) === itemId);
|
||||
const cachedItem = idx !== -1 ? currentItems[idx] : {};
|
||||
|
||||
const newItem = renderItem(window.currentItems[idx], window.IS_SHARE);
|
||||
const oldItem = document.getElementById(`item-${data.item_id}`);
|
||||
if (oldItem && newItem) {
|
||||
oldItem.replaceWith(newItem);
|
||||
}
|
||||
// Note: keep the edited item visible immediately, even when the local list cache is stale.
|
||||
const updatedItem = {
|
||||
...cachedItem,
|
||||
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 || '',
|
||||
note: cachedItem.note || ''
|
||||
};
|
||||
|
||||
if (idx !== -1) {
|
||||
currentItems[idx] = updatedItem;
|
||||
window.currentItems = currentItems;
|
||||
}
|
||||
|
||||
showToast(`Zaktualizowano produkt: ${data.new_name} (x${data.new_quantity})`, 'success');
|
||||
if (oldItem) {
|
||||
oldItem.replaceWith(renderItem(updatedItem, window.IS_SHARE));
|
||||
} else if (window.LIST_ID) {
|
||||
socket.emit('request_full_list', { list_id: window.LIST_ID });
|
||||
}
|
||||
|
||||
showToast(`Zaktualizowano produkt: ${data.new_name} (${data.quantity_label || quantityLabel(data.quantity_value ?? data.new_quantity, data.quantity_unit || 'szt')})`, 'success');
|
||||
|
||||
updateProgressBar();
|
||||
toggleEmptyPlaceholder();
|
||||
applyHidePurchased();
|
||||
});
|
||||
|
||||
// --- WAŻNE: zapisz dane do reconnect ---
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -99,7 +99,7 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
summary.innerHTML = `
|
||||
<p class="mb-1">📦 <strong>${totalCount}</strong> produktów</p>
|
||||
<p class="mb-1">✅ Kupione: <strong>${purchasedCount}</strong> (${percent}%)</p>
|
||||
<p class="mb-0">💸 Wydatek: <strong>${totalExpense.toFixed(2)} zł</strong></p>`;
|
||||
<p class="mb-0">💸 Wydatek: <strong>${formatCurrencyAmount(totalExpense)}</strong></p>`;
|
||||
productList.appendChild(summary);
|
||||
|
||||
const purchased = createSection("✔️ Kupione");
|
||||
|
||||
@@ -22,7 +22,7 @@ async function analyzeReceipts(listId) {
|
||||
|
||||
let html = `<div class="card bg-dark text-white border-secondary p-3">`;
|
||||
html += `<p class="text-secondary"><small>⏱ Czas analizy OCR: ${duration} sek.</small></p>`;
|
||||
html += `<p><b>📊 Łącznie wykryto:</b> ${data.total.toFixed(2)} PLN</p>`;
|
||||
html += `<p><b>📊 Łącznie wykryto:</b> ${formatCurrencyAmount(data.total)}</p>`;
|
||||
|
||||
data.results.forEach((r, i) => {
|
||||
const disabled = r.already_added ? "disabled" : "";
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
new TomSelect("#categories", {
|
||||
const categoriesSelect = document.querySelector("#categories");
|
||||
|
||||
if (!categoriesSelect || typeof TomSelect === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
new TomSelect(categoriesSelect, {
|
||||
plugins: ['remove_button'],
|
||||
maxItems: 1,
|
||||
placeholder: 'Wybierz jedną kategorie...',
|
||||
placeholder: 'Wybierz jedną kategorię...',
|
||||
create: false,
|
||||
dropdownParent: 'body',
|
||||
sortField: {
|
||||
field: "text",
|
||||
direction: "asc"
|
||||
|
||||
@@ -12,7 +12,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
total += parseFloat(cb.dataset.amount);
|
||||
}
|
||||
});
|
||||
totalEl.textContent = total.toFixed(2) + ' PLN';
|
||||
totalEl.textContent = formatCurrencyAmount(total);
|
||||
}
|
||||
|
||||
selectAllBtn.addEventListener('click', () => {
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
|
||||
<span class="progress-label main-list-progress__label small fw-bold {% if percent < 51 %}text-white{% else %}text-dark{% endif %}">
|
||||
Produkty: {{ purchased_count }}/{{ total_count }} ({{ percent|round(0)|int }}%)
|
||||
{% if total_expense > 0 %} — 💸 {{ '%.2f'|format(total_expense) }} PLN{% endif %}
|
||||
{% if total_expense > 0 %} — 💸 {{ format_currency(total_expense) }}{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -51,7 +51,7 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<td>💸 Średnia kwota na listę</td>
|
||||
<td class="text-end fw-bold">{{ avg_list_expense }} zł</td>
|
||||
<td class="text-end fw-bold">{{ format_currency(avg_list_expense) }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -115,30 +115,30 @@
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Wszystkie</td>
|
||||
<td>{{ '%.2f'|format(expense_summary.all.month) }} PLN</td>
|
||||
<td>{{ '%.2f'|format(expense_summary.all.year) }} PLN</td>
|
||||
<td>{{ '%.2f'|format(expense_summary.all.total) }} PLN</td>
|
||||
<td>{{ format_currency(expense_summary.all.month) }}</td>
|
||||
<td>{{ format_currency(expense_summary.all.year) }}</td>
|
||||
<td>{{ format_currency(expense_summary.all.total) }}</td>
|
||||
<!-- <td>{{ '%.2f'|format(expense_summary.all.avg) }} PLN</td> -->
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Aktywne</td>
|
||||
<td>{{ '%.2f'|format(expense_summary.active.month) }} PLN</td>
|
||||
<td>{{ '%.2f'|format(expense_summary.active.year) }} PLN</td>
|
||||
<td>{{ '%.2f'|format(expense_summary.active.total) }} PLN</td>
|
||||
<td>{{ format_currency(expense_summary.active.month) }}</td>
|
||||
<td>{{ format_currency(expense_summary.active.year) }}</td>
|
||||
<td>{{ format_currency(expense_summary.active.total) }}</td>
|
||||
<!-- <td>{{ '%.2f'|format(expense_summary.active.avg) }} PLN</td> -->
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Archiwalne</td>
|
||||
<td>{{ '%.2f'|format(expense_summary.archived.month) }} PLN</td>
|
||||
<td>{{ '%.2f'|format(expense_summary.archived.year) }} PLN</td>
|
||||
<td>{{ '%.2f'|format(expense_summary.archived.total) }} PLN</td>
|
||||
<td>{{ format_currency(expense_summary.archived.month) }}</td>
|
||||
<td>{{ format_currency(expense_summary.archived.year) }}</td>
|
||||
<td>{{ format_currency(expense_summary.archived.total) }}</td>
|
||||
<!-- <td>{{ '%.2f'|format(expense_summary.archived.avg) }} PLN</td> -->
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Wygasłe</td>
|
||||
<td>{{ '%.2f'|format(expense_summary.expired.month) }} PLN</td>
|
||||
<td>{{ '%.2f'|format(expense_summary.expired.year) }} PLN</td>
|
||||
<td>{{ '%.2f'|format(expense_summary.expired.total) }} PLN</td>
|
||||
<td>{{ format_currency(expense_summary.expired.month) }}</td>
|
||||
<td>{{ format_currency(expense_summary.expired.year) }}</td>
|
||||
<td>{{ format_currency(expense_summary.expired.total) }}</td>
|
||||
<!-- <td>{{ '%.2f'|format(expense_summary.expired.avg) }} PLN</td> -->
|
||||
</tr>
|
||||
</tbody>
|
||||
@@ -282,7 +282,7 @@
|
||||
{% if e.total_expense >= 500 %}text-danger
|
||||
{% elif e.total_expense > 0 %}text-success{% endif %}">
|
||||
{% if e.total_expense > 0 %}
|
||||
{{ '%.2f'|format(e.total_expense) }} PLN
|
||||
{{ format_currency(e.total_expense) }}
|
||||
{% else %}
|
||||
-
|
||||
{% endif %}
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
<!-- Wydatek i właściciel -->
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<label for="amount" class="form-label">💰 Całkowity wydatek (PLN)</label>
|
||||
<label for="amount" class="form-label">💰 Całkowity wydatek ({{ CURRENCY_CODE }})</label>
|
||||
<input type="number" step="0.01" min="0" class="form-control bg-dark text-white border-secondary ui-consistent-input"
|
||||
id="amount" name="amount" value="{{ '%.2f'|format(total_expense) }}">
|
||||
</div>
|
||||
|
||||
@@ -173,6 +173,10 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<script>
|
||||
window.CURRENCY_CODE = {{ CURRENCY_CODE|tojson }};
|
||||
</script>
|
||||
|
||||
{% if request.endpoint != 'system_auth' %}
|
||||
<script src="{{ static_asset_url('static_bp.serve_js_lib', 'glightbox.min.js') }}"></script>
|
||||
<script src="{{ static_asset_url('static_bp.serve_js_lib', 'socket.io.min.js') }}"></script>
|
||||
|
||||
@@ -88,7 +88,7 @@
|
||||
<button id="deselectAllBtn" class="btn btn-sm btn-outline-light active" style="display: none;">Odznacz
|
||||
wszystko</button>
|
||||
</div>
|
||||
<h5 class="text-success m-0">💰 Suma: <span id="listsTotal">0.00 PLN</span></h5>
|
||||
<h5 class="text-success m-0">💰 Suma: <span id="listsTotal">{{ format_currency(0) }}</span></h5>
|
||||
</div>
|
||||
|
||||
<!-- Tabela list z możliwością filtrowania -->
|
||||
@@ -101,7 +101,7 @@
|
||||
<th>Nazwa listy</th>
|
||||
<th>Właściciel</th>
|
||||
<th>Data</th>
|
||||
<th>Wydatki (PLN)</th>
|
||||
<th>Wydatki ({{ CURRENCY_CODE }})</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="listsTableBody">
|
||||
|
||||
@@ -96,11 +96,11 @@
|
||||
<br>
|
||||
{% if total_expense > 0 %}
|
||||
<div id="total-expense2" class="text-success fw-bold mb-3">
|
||||
💸 Łącznie wydano: {{ '%.2f'|format(total_expense) }} PLN
|
||||
💸 Łącznie wydano: {{ format_currency(total_expense) }}
|
||||
</div>
|
||||
{% else %}
|
||||
<div id="total-expense2" class="text-success fw-bold mb-3">
|
||||
💸 Łącznie wydano: 0.00 PLN
|
||||
💸 Łącznie wydano: {{ format_currency(0) }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
@@ -125,9 +125,18 @@
|
||||
<div class="shopping-item-content">
|
||||
<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>
|
||||
<button type="button"
|
||||
id="name-{{ item.id }}"
|
||||
class="shopping-item-name text-white"
|
||||
data-item-id="{{ item.id }}"
|
||||
data-item-name={{ item.name|tojson }}
|
||||
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>
|
||||
{% 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 %}
|
||||
@@ -140,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 %}
|
||||
@@ -166,6 +176,11 @@
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
<div id="desktopItemMenu" hidden>
|
||||
<button type="button" class="btn btn-outline-light btn-sm w-100 text-start" data-menu-action="edit">✏️ Edytuj</button>
|
||||
<button type="button" class="btn btn-outline-danger btn-sm w-100 text-start" data-menu-action="delete">🗑️ Usuń</button>
|
||||
</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">
|
||||
@@ -181,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>
|
||||
|
||||
@@ -210,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"
|
||||
@@ -541,30 +577,11 @@
|
||||
<script src="{{ static_asset_url('static_bp.serve_js', 'access_users.js') }}"></script>
|
||||
<script src="{{ static_asset_url('static_bp.serve_js', 'category_modal.js') }}"></script>
|
||||
<script src="{{ static_asset_url('static_bp.serve_js', 'notes.js') }}"></script>
|
||||
<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 %}
|
||||
@@ -12,11 +12,11 @@
|
||||
|
||||
{% if total_expense > 0 %}
|
||||
<span id="total-expense1" class="badge rounded-pill bg-success ms-2">
|
||||
💸 {{ '%.2f'|format(total_expense) }} PLN
|
||||
💸 {{ format_currency(total_expense) }}
|
||||
</span>
|
||||
{% else %}
|
||||
<span id="total-expense" class="badge rounded-pill bg-secondary ms-2" style="display: none;">
|
||||
💸 0.00 PLN
|
||||
💸 {{ format_currency(0) }}
|
||||
</span>
|
||||
{% endif %}
|
||||
|
||||
@@ -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>
|
||||
@@ -114,7 +126,7 @@
|
||||
<span>💰 Dodaj wydatek</span>
|
||||
|
||||
<span class="badge rounded-pill bg-success" id="total-expense2">
|
||||
💸 Łączna suma: {{ '%.2f'|format(total_expense) }} PLN
|
||||
💸 Łączna suma: {{ format_currency(total_expense) }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -123,7 +135,7 @@
|
||||
<div class="input-group mb-0 shopping-compact-input-group shopping-expense-input-group">
|
||||
<input id="expenseAmount" type="number" step="0.01" min="0"
|
||||
class="form-control bg-dark text-white border-secondary shopping-expense-amount-input"
|
||||
placeholder="Kwota (PLN)">
|
||||
placeholder="{{ currency_placeholder() }}">
|
||||
|
||||
<button onclick="submitExpense({{ list.id }})"
|
||||
class="btn btn-outline-primary share-submit-btn share-submit-btn--expense shopping-compact-submit">
|
||||
@@ -135,7 +147,7 @@
|
||||
{% endif %}
|
||||
|
||||
<p id="total-expense2" style="display: none;">
|
||||
<b>💸 Łącznie wydano:</b> {{ '%.2f'|format(total_expense) }} PLN
|
||||
<b>💸 Łącznie wydano:</b> {{ format_currency(total_expense) }}
|
||||
</p>
|
||||
|
||||
<button id="toggleReceiptBtn" type="button" class="receipt-disclosure mb-3"
|
||||
@@ -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 %}
|
||||
@@ -101,7 +101,7 @@
|
||||
</div>
|
||||
<div class="main-summary-stat">
|
||||
<span class="main-summary-stat__label">Wydatki</span>
|
||||
<strong>{{ '%.2f'|format(summary.total_expense) }} PLN</strong>
|
||||
<strong>{{ format_currency(summary.total_expense) }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -26,6 +26,9 @@ def inject_version():
|
||||
|
||||
return {
|
||||
"APP_VERSION": app.config["APP_VERSION"],
|
||||
"CURRENCY_CODE": get_currency_code(),
|
||||
"format_currency": format_currency,
|
||||
"currency_placeholder": currency_placeholder,
|
||||
"static_asset_url": static_asset_url,
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user