diff --git a/app.py b/app.py index 57469b5..03da900 100644 --- a/app.py +++ b/app.py @@ -103,7 +103,7 @@ def get_db(): def init_db(): with get_db() as c: c.execute("""CREATE TABLE IF NOT EXISTS prices ( - key TEXT PRIMARY KEY, price REAL, checked INT DEFAULT 0, updated TEXT)""") + key TEXT PRIMARY KEY, price REAL, checked INT DEFAULT 0, qty TEXT, updated TEXT)""") c.execute("""CREATE TABLE IF NOT EXISTS extras ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, section_key TEXT, price REAL, @@ -115,6 +115,10 @@ def init_db(): c.execute("ALTER TABLE extras ADD COLUMN section_key TEXT") except Exception: pass + try: + c.execute("ALTER TABLE prices ADD COLUMN qty TEXT") + except Exception: + pass @app.route('/') @@ -136,13 +140,14 @@ def save_price(): key = d.get('key') price = d.get('price') checked = int(d.get('checked', 0)) + qty = d.get('qty') or None if not key: return jsonify(error='missing key'), 400 with get_db() as c: - c.execute("""INSERT INTO prices(key,price,checked,updated) VALUES(?,?,?,?) + c.execute("""INSERT INTO prices(key,price,checked,qty,updated) VALUES(?,?,?,?,?) ON CONFLICT(key) DO UPDATE SET price=excluded.price, - checked=excluded.checked, updated=excluded.updated""", - (key, price, checked, datetime.now().isoformat())) + checked=excluded.checked, qty=excluded.qty, updated=excluded.updated""", + (key, price, checked, qty, datetime.now().isoformat())) return jsonify(ok=True) diff --git a/templates/index.html b/templates/index.html index 23d7832..eb46004 100644 --- a/templates/index.html +++ b/templates/index.html @@ -47,7 +47,7 @@ .section-header.custom-sec { background: var(--accent); } .items-table { background: var(--card); border: 1px solid var(--border); border-top: none; border-radius: 0 0 8px 8px; overflow: hidden; } - .item-row { display: grid; grid-template-columns: 28px 1fr 56px 82px 86px; align-items: center; border-bottom: 1px solid var(--border); transition: background 0.15s; } + .item-row { display: grid; grid-template-columns: 28px 1fr 72px 82px 86px; align-items: center; border-bottom: 1px solid var(--border); transition: background 0.15s; } .item-row:last-child { border-bottom: none; } .item-row:hover { background: #faf9f7; } .item-row.checked { opacity: 0.4; } @@ -59,14 +59,18 @@ .badge { display: inline-block; font-size: 9px; padding: 1px 5px; border-radius: 3px; margin-left: 5px; vertical-align: middle; font-weight: 500; } .badge-card { background: var(--blue-light); color: var(--blue); } .badge-kasse { background: var(--amber-light); color: var(--amber); } - .item-qty { padding: 0.7rem 0.5rem; font-size: 11px; color: var(--muted); font-family: var(--mono); text-align: center; } + .item-qty { padding: 0.35rem 0.4rem; } + .item-qty input { width: 100%; border: 1px solid transparent; border-radius: 5px; padding: 4px 5px; font-size: 11px; font-family: var(--mono); background: transparent; color: var(--muted); text-align: center; outline: none; transition: border-color 0.15s, background 0.15s; cursor: text; } + .item-qty input:hover { border-color: var(--border); background: var(--bg); } + .item-qty input:focus { border-color: var(--accent); background: white; color: var(--text); } + .item-qty input.modified { color: var(--accent); font-weight: 500; } .item-est { padding: 0.7rem 0.5rem; font-size: 12px; font-family: var(--mono); color: var(--muted); text-align: right; } .item-actual { padding: 0.45rem 0.75rem 0.45rem 0.5rem; } .item-actual input { width: 100%; border: 1px solid var(--border); border-radius: 6px; padding: 5px 8px; font-size: 13px; font-family: var(--mono); background: var(--bg); color: var(--text); text-align: right; outline: none; transition: border-color 0.15s, background 0.15s; } .item-actual input:focus { border-color: var(--accent); background: white; } .item-actual input.has-value { background: var(--green-light); border-color: #a8d5bc; color: var(--green); font-weight: 500; } - .col-headers { display: grid; grid-template-columns: 28px 1fr 56px 82px 86px; background: #f0ece6; border: 1px solid var(--border); border-top: none; padding: 3px 0; } + .col-headers { display: grid; grid-template-columns: 28px 1fr 72px 82px 86px; background: #f0ece6; border: 1px solid var(--border); border-top: none; padding: 3px 0; } .col-h { font-size: 9px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.07em; font-weight: 500; } .col-h:nth-child(1) { padding-left: 0.875rem; } .col-h:nth-child(2) { padding-left: 0.5rem; } @@ -144,9 +148,9 @@ .stats-row { grid-template-columns: repeat(2, 1fr); } header { grid-template-columns: 1fr; } .header-actions { flex-direction: row; align-items: center; } - .item-row { grid-template-columns: 28px 1fr 50px 78px; } + .item-row { grid-template-columns: 28px 1fr 64px 78px; } .item-est { display: none; } - .col-headers { grid-template-columns: 28px 1fr 50px 78px; } + .col-headers { grid-template-columns: 28px 1fr 64px 78px; } .col-h:nth-child(4) { display: none; } .extra-item-row { grid-template-columns: 28px 1fr auto auto; } } @@ -228,7 +232,11 @@ {% if item.get('badges') %}{% for b in item.badges %}{% if b == 'card' %}CITTI CARD{% endif %}{% if b == 'kasse' %}Hel kasse{% endif %}{% endfor %}{% endif %} {% if item.get('note') %}{{ item.note }}{% endif %} -
{{ item.qty }}
+
+ +
{{ "{:.2f}".format(item.est).replace(".",",") if item.est else "—" }}
@@ -438,13 +446,27 @@ function updateActual(input) { } function saveToServer(key, price, checked) { + const row = document.querySelector(`.item-row[data-key="${key}"]`); + const qtyInput = row ? row.querySelector('.item-qty input') : null; + const qty = qtyInput ? qtyInput.value.trim() || null : null; fetch('/api/price', { method: 'POST', headers: {'Content-Type':'application/json'}, - body: JSON.stringify({key, price, checked}) + body: JSON.stringify({key, price, checked, qty}) }).then(() => showSaved()); } +function updateQty(input) { + const row = input.closest('.item-row'); + const key = row.dataset.key; + const defaultQty = input.placeholder; + input.classList.toggle('modified', input.value.trim() !== '' && input.value.trim() !== defaultQty); + const price = parseFloat(row.querySelector('.item-actual input').value); + const checked = row.querySelector('input[type=checkbox]').checked ? 1 : 0; + clearTimeout(debounceTimers[key + '_qty']); + debounceTimers[key + '_qty'] = setTimeout(() => saveToServer(key, isNaN(price) ? null : price, checked), 600); +} + function recalcAll() { const sections = document.querySelectorAll('.items-table'); let totalActual = 0; let hasAny = false;