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 @@
-
+