feat(shopping): editable quantity field per item
All checks were successful
Build and Deploy citti / build-and-deploy (push) Successful in 26s
All checks were successful
Build and Deploy citti / build-and-deploy (push) Successful in 26s
Each item row now has an editable qty input showing the planned amount (e.g. "6 × ½ kg"). Changes are auto-saved via debounce to the prices table and highlighted in accent color when modified from the default. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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' %}<span class="badge badge-card">CITTI CARD</span>{% endif %}{% if b == 'kasse' %}<span class="badge badge-kasse">Hel kasse</span>{% endif %}{% endfor %}{% endif %}
|
||||
{% if item.get('note') %}<span class="note">{{ item.note }}</span>{% endif %}
|
||||
</div>
|
||||
<div class="item-qty">{{ item.qty }}</div>
|
||||
<div class="item-qty">
|
||||
<input type="text" value="{{ s.get('qty') or item.qty }}" placeholder="{{ item.qty }}"
|
||||
oninput="updateQty(this)" onchange="updateQty(this)"
|
||||
{% if s.get('qty') and s.get('qty') != item.qty %}class="modified"{% endif %}>
|
||||
</div>
|
||||
<div class="item-est">{{ "{:.2f}".format(item.est).replace(".",",") if item.est else "—" }}</div>
|
||||
<div class="item-actual">
|
||||
<input type="number" placeholder="—" value="{{ s.get('price') or '' }}" oninput="updateActual(this)" step="0.01" min="0">
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user