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
This commit is contained in:
dadaloop82
2026-05-04 16:56:37 +00:00
parent 874ae90afa
commit bff22d43b4
6 changed files with 51 additions and 12 deletions
+8
View File
@@ -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 {
+34 -6
View File
@@ -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,
+1 -1
View File
@@ -1318,6 +1318,6 @@
</div>
</div>
<script src="assets/js/app.js?v=20260504b"></script>
<script src="assets/js/app.js?v=20260504c"></script>
</body>
</html>
+3 -2
View File
@@ -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": "Ø",
+2 -1
View File
@@ -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",
+3 -2
View File
@@ -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",