From bff22d43b4c281824f3e277e39f5205cbcc3bdc8 Mon Sep 17 00:00:00 2001 From: dadaloop82 Date: Mon, 4 May 2026 16:56:37 +0000 Subject: [PATCH] fix(i18n): translate antiwaste title in IT/DE + use quantity validation - translations: antiwaste.title = 'Rapporto Anti-Spreco' (IT), 'Anti-Verschwendungs-Bericht' (DE) - translations: add use.error_exceeds_stock in IT/EN/DE - submitUse(): block submit if qty > available at selected location + shake animation - adjustUseQty(): cap '+' button at max available at selected location (incl. sub-unit mode) - style.css: add @keyframes inputShake + .input-shake class Bump app.js cache buster to v=20260504c --- assets/css/style.css | 8 ++++++++ assets/js/app.js | 40 ++++++++++++++++++++++++++++++++++------ index.html | 2 +- translations/de.json | 5 +++-- translations/en.json | 3 ++- translations/it.json | 5 +++-- 6 files changed, 51 insertions(+), 12 deletions(-) diff --git a/assets/css/style.css b/assets/css/style.css index c660e32..a17f239 100644 --- a/assets/css/style.css +++ b/assets/css/style.css @@ -384,6 +384,14 @@ body { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } } +@keyframes inputShake { + 0%, 100% { transform: translateX(0); } + 20% { transform: translateX(-6px); } + 40% { transform: translateX(6px); } + 60% { transform: translateX(-4px); } + 80% { transform: translateX(4px); } +} +.input-shake { animation: inputShake 0.5s ease; border-color: var(--danger, #ef4444) !important; } /* ===== PAGE HEADER ===== */ .page-header { diff --git a/assets/js/app.js b/assets/js/app.js index a2c72d4..826536f 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -6739,7 +6739,23 @@ function adjustUseQty(direction) { } } val = Math.max(step, val + direction * step); - input.value = Math.round(val * 1000) / 1000; + val = Math.round(val * 1000) / 1000; + + // Cap at max available at selected location (in current unit) + const selectedLoc = document.getElementById('use-location')?.value; + if (selectedLoc && _useCurrentItems.length > 0) { + const locItems = _useCurrentItems.filter(i => i.location === selectedLoc); + const maxQtyAtLoc = locItems.reduce((s, i) => s + parseFloat(i.quantity || 0), 0); + if (maxQtyAtLoc > 0) { + // Convert to sub-unit for comparison if needed + const maxInCurrentUnit = (_useConfMode && _useConfMode._activeUnit === 'sub') + ? maxQtyAtLoc * _useConfMode.packageSize + : maxQtyAtLoc; + val = Math.min(val, Math.round(maxInCurrentUnit * 1000) / 1000); + } + } + + input.value = val; // Sync fraction button highlight if visible const newVal = parseFloat(input.value); document.querySelectorAll('#pz-fraction-btns .frac-btn').forEach(b => { @@ -7235,12 +7251,8 @@ async function submitUse(e) { e.preventDefault(); if (_useSubmitting) return; // prevent double-submit from scale auto-confirm _useSubmitting = true; - // Stop timers but KEEP _scaleLastConfirmedGrams: this prevents the scale from - // re-triggering another auto-submit while the product is still on the plate. - // (Calling _cancelScaleAutoConfirm(false) would reset the sentinel to null, - // allowing the same weight to start a new 10-second cycle immediately.) _cancelScaleTimersOnly(); - _scaleStabilityVal = null; // reset sentinel so a new DIFFERENT weight restarts correctly + _scaleStabilityVal = null; showLoading(true); try { let qty = parseFloat(document.getElementById('use-quantity').value) || 1; @@ -7254,6 +7266,22 @@ async function submitUse(e) { } else if (_useConfMode && _useConfMode._activeUnit === 'conf') { displayUnit = 'conf'; } + + // ── Validate: cannot use more than available at selected location ───────── + const selectedLoc = document.getElementById('use-location').value; + const locItems = _useCurrentItems.filter(i => i.location === selectedLoc); + const maxQtyAtLoc = locItems.reduce((s, i) => s + parseFloat(i.quantity || 0), 0); + if (maxQtyAtLoc > 0 && qty > maxQtyAtLoc + 0.001) { + showLoading(false); + _useSubmitting = false; + showToast(t('use.error_exceeds_stock'), 'error'); + // Shake the input to make it obvious + const inp = document.getElementById('use-quantity'); + inp.classList.add('input-shake'); + setTimeout(() => inp.classList.remove('input-shake'), 600); + return; + } + // ───────────────────────────────────────────────────────────────────────── const result = await api('inventory_use', {}, 'POST', { product_id: currentProduct.id, diff --git a/index.html b/index.html index 548eef9..97d7d83 100644 --- a/index.html +++ b/index.html @@ -1318,6 +1318,6 @@ - + diff --git a/translations/de.json b/translations/de.json index ce247f3..5f051e2 100644 --- a/translations/de.json +++ b/translations/de.json @@ -242,7 +242,8 @@ "toast_bring": "🛒 Produkt aufgebraucht → zu Bring! hinzugefügt", "toast_opened_finished": "🔓 Geöffnete Packung von {name} aufgebraucht!", "disambiguation_hint": "Was meinst du mit \"alles aufgebraucht\"?", - "disambiguation_all": "🗑️ ALLES aufgebraucht ({qty})" + "disambiguation_all": "🗑️ ALLES verbraucht ({qty})", + "error_exceeds_stock": "⚠️ Du kannst nicht mehr verwenden als du verfügbar hast!" }, "product": { "title_new": "Neues Produkt", @@ -691,7 +692,7 @@ "item_added": "{name} hinzugefügt" }, "antiwaste": { - "title": "🌱 Anti-Waste Report", + "title": "🌱 Anti-Verschwendungs-Bericht", "grade_label": "Note", "you": "Du", "avg_label": "Ø", diff --git a/translations/en.json b/translations/en.json index 58d3a74..b636ab1 100644 --- a/translations/en.json +++ b/translations/en.json @@ -241,7 +241,8 @@ "toast_bring": "🛒 Product finished → added to Bring!", "toast_opened_finished": "🔓 Opened package of {name} finished!", "disambiguation_hint": "What do you mean by \"all done\"?", - "disambiguation_all": "🗑️ Finish EVERYTHING ({qty})" + "disambiguation_all": "🗑️ Finish EVERYTHING ({qty})", + "error_exceeds_stock": "⚠️ You cannot use more than you have available!" }, "product": { "title_new": "New Product", diff --git a/translations/it.json b/translations/it.json index 0b3897c..4a2af70 100644 --- a/translations/it.json +++ b/translations/it.json @@ -241,7 +241,8 @@ "toast_bring": "🛒 Prodotto finito → aggiunto a Bring!", "toast_opened_finished": "🔓 Confezione aperta di {name} finita!", "disambiguation_hint": "Cosa intendi con \"finito tutto\"?", - "disambiguation_all": "🗑️ Finito TUTTO ({qty})" + "disambiguation_all": "🗑️ Finito TUTTO ({qty})", + "error_exceeds_stock": "⚠️ Non puoi usare più di quanto hai disponibile!" }, "product": { "title_new": "Nuovo Prodotto", @@ -690,7 +691,7 @@ "item_added": "{name} aggiunto" }, "antiwaste": { - "title": "🌱 Anti-Waste Report", + "title": "🌱 Rapporto Anti-Spreco", "grade_label": "Voto", "you": "Tu", "avg_label": "Media",