fix(pos): keep edit-line modal from getting stuck on invalid qty; show clear 409 stock-shortage error instead of silently queuing offline
This commit is contained in:
parent
46d5d540a0
commit
79cc7d1f0a
2 changed files with 63 additions and 3 deletions
|
|
@ -618,7 +618,20 @@ function editSave(){
|
|||
if (!line) { closeEditLine(); return; }
|
||||
const qty = Number((document.getElementById('editQty').value || '').replace(/[^0-9.]/g, ''));
|
||||
const price = Number((document.getElementById('editPrice').value || '').replace(/[^0-9.]/g, ''));
|
||||
if (!Number.isFinite(qty) || qty <= 0) { return; }
|
||||
const hint = document.getElementById('editHint');
|
||||
// Bad qty — show an error in the hint, keep the modal open so the
|
||||
// operator can fix it. (Previously the modal stayed open without
|
||||
// any feedback, so a cleared/0 qty field left the screen grey and
|
||||
// the operator had to long-press another line to escape.)
|
||||
if (!Number.isFinite(qty) || qty <= 0) {
|
||||
if (hint) {
|
||||
hint.textContent = 'Enter a quantity of 1 or more.';
|
||||
hint.style.color = '#c92a2a';
|
||||
}
|
||||
const qIn = document.getElementById('editQty');
|
||||
if (qIn) { qIn.focus(); qIn.select(); }
|
||||
return;
|
||||
}
|
||||
// price === 0 is allowed (the operator can comp a line) but
|
||||
// negative isn't. If the input is empty, leave the price alone.
|
||||
const shelf = Number(line.p.selling_price ?? line.p.cost_price ?? 0) || 0;
|
||||
|
|
@ -627,6 +640,9 @@ function editSave(){
|
|||
else delete line.unitPriceOverride; // back to default
|
||||
}
|
||||
line.qty = Math.floor(qty);
|
||||
// Reset the hint to its neutral default before closing so the next
|
||||
// openEditLine() doesn't show a stale error from this round.
|
||||
if (hint) { hint.textContent = 'Hold to edit again, or change a value and tap Save.'; hint.style.color = ''; }
|
||||
persistCart();
|
||||
render();
|
||||
renderCartDrawer();
|
||||
|
|
@ -831,6 +847,14 @@ async function confirmSale(){
|
|||
discount_input: state.discount && state.discount.kind !== 'none' ? state.discount.input : undefined,
|
||||
};
|
||||
|
||||
// Distinguish a permanent server-side rejection (insufficient
|
||||
// stock, bad input) from a transient failure (server down, network
|
||||
// blip). The former must NOT advance to the receipt page or get
|
||||
// queued for offline retry — it's a real, human-fixable problem
|
||||
// the operator needs to see. The latter is what the offline queue
|
||||
// is for.
|
||||
let serverError = null;
|
||||
|
||||
try{
|
||||
const r = await window.posFetch('/checkout', {
|
||||
method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify(checkoutBody),
|
||||
|
|
@ -840,7 +864,18 @@ async function confirmSale(){
|
|||
sale.synced = true;
|
||||
sale.serverRef = j.sale && j.sale.id;
|
||||
} else {
|
||||
throw new Error('server returned ' + r.status);
|
||||
// Try to read the body so we can tell apart stock-shortage
|
||||
// (409) from generic 4xx/5xx. The route returns JSON on every
|
||||
// non-2xx path, so this should always parse.
|
||||
let body = null;
|
||||
try { body = await r.json(); } catch (_) { /* not json */ }
|
||||
if (body && body.reason === 'insufficient_stock' && Array.isArray(body.shortages) && body.shortages.length) {
|
||||
serverError = { kind: 'insufficient_stock', shortages: body.shortages };
|
||||
} else {
|
||||
const msg = body && body.message ? body.message : ('server returned ' + r.status);
|
||||
serverError = { kind: 'other', message: msg };
|
||||
throw new Error(msg);
|
||||
}
|
||||
}
|
||||
}catch(e){
|
||||
console.warn('Sale queued for sync', e);
|
||||
|
|
@ -856,6 +891,31 @@ async function confirmSale(){
|
|||
}
|
||||
}
|
||||
|
||||
// Permanent server-side rejection — stop here, do NOT navigate to
|
||||
// the receipt page, do NOT queue for retry. Show a clear error,
|
||||
// re-enable the Complete button so the operator can fix the
|
||||
// cart (remove a line, lower the qty) and try again. The cart
|
||||
// stays open.
|
||||
if (serverError) {
|
||||
if (serverError.kind === 'insufficient_stock') {
|
||||
const lines = serverError.shortages.map(s => {
|
||||
const item = state.cart.get(s.stock_item_id);
|
||||
const name = item && item.p
|
||||
? (item.p.description || item.p.sku || s.stock_item_id)
|
||||
: s.stock_item_id;
|
||||
return `${name}: only ${s.available} in stock (you asked for ${s.requested})`;
|
||||
});
|
||||
toast.error('Not enough stock — ' + lines.join('; '));
|
||||
} else {
|
||||
toast.error('Sale rejected: ' + (serverError.kind === 'other' ? serverError.message : 'Unknown error'));
|
||||
}
|
||||
if (btn) {
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M5 12l5 5 9-11"/></svg>Complete sale';
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// ALWAYS navigate to receipt — never leave the user stuck on "Saving…"
|
||||
try{ sessionStorage.setItem('pos.lastSale', JSON.stringify(sale)); }catch(_){}
|
||||
state.cart.clear(); persistCart(); paintCart(); render();
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
// Service worker — cache app shell, network-first for HTML + theme.js, cache-first for other static.
|
||||
const CACHE = "pos-v18";
|
||||
const CACHE = "pos-v19";
|
||||
const APP_SHELL = [
|
||||
'./',
|
||||
'./index.html',
|
||||
|
|
|
|||
Loading…
Reference in a new issue