diff --git a/frontend/modules/expiryReport.html b/frontend/modules/expiryReport.html
index cbdf27f6f..07b53660c 100644
--- a/frontend/modules/expiryReport.html
+++ b/frontend/modules/expiryReport.html
@@ -8,5 +8,5 @@
').append($('').text(PosnicPro.i18n.t('lang_no_products_expiry_review', 'No products to review.'))).appendTo('#expiry_report_rows');
$('#expiry_report_page').text(page + ' / ' + Math.max(1, Math.ceil(d.total / 25)));
- $('#expiry_report_previous').prop('disabled', page <= 1);
- $('#expiry_report_next').prop('disabled', page * 25 >= d.total);
+ $('#expiry_report_paging').toggleClass('d-flex', d.total > 25).toggleClass('d-none', d.total <= 25);
+ $('#expiry_report_previous').toggle(page > 1).prop('disabled', page <= 1);
+ $('#expiry_report_next').toggle(page * 25 < d.total).prop('disabled', page * 25 >= d.total);
}, function () { if (ticket === request) { $('#expiry_report_summary').text(''); failed(); } });
}
};
diff --git a/frontend/static/script/js/modules/js/lowstock.js b/frontend/static/script/js/modules/js/lowstock.js
index 6cda7a1f2..58f0bdd17 100644
--- a/frontend/static/script/js/modules/js/lowstock.js
+++ b/frontend/static/script/js/modules/js/lowstock.js
@@ -168,7 +168,7 @@ PosnicPro.lowstockitems = {
};
var html = '';
if (pages > 1) {
- html += btn(p - 1, '«', p <= 1);
+ if (p > 1) { html += btn(p - 1, '«', false); }
var end = Math.min(pages, Math.max(1, p - 2) + 4);
var start = Math.max(1, end - 4);
for (var n = start; n <= end; n++) {
@@ -176,7 +176,7 @@ PosnicPro.lowstockitems = {
}
}
html += '' + label + '';
- if (pages > 1) { html += btn(p + 1, '»', p >= pages); }
+ if (p < pages) { html += btn(p + 1, '»', false); }
$('#lowstockitems_list_paging').html(html);
},
goPage: function (n) {
diff --git a/frontend/static/script/js/modules/js/sale-extensions.js b/frontend/static/script/js/modules/js/sale-extensions.js
index a8dc05db4..f526c32da 100644
--- a/frontend/static/script/js/modules/js/sale-extensions.js
+++ b/frontend/static/script/js/modules/js/sale-extensions.js
@@ -71,39 +71,48 @@
var escaped=message.slice(0,300).replace(/&/g,'&').replace(//g,'>');
PosnicPro.alert('success',escaped,3500);
}
- function refreshStock(done) {
- if(!PosnicPro.sales.loadBillingCatalogue){done?.(false);return;}
+ // Read only the visible/saved products; never reload the whole catalogue.
+ async function freshProducts(ids) {
+ var unique = Array.from(new Set(ids)), result = new Map(), cursor = 0;
+ await Promise.all(Array.from({length:Math.min(6,unique.length)}, async function () {
+ while(cursor < unique.length) {
+ var id=unique[cursor++];
+ var item=await new Promise(function(resolve) {
+ PosnicPro.get('items/'+id,function(response){resolve(response?.type==='success'?response.data:null);},function(){resolve(null);});
+ });
+ result.set(id,item);
+ }
+ }));
+ return result;
+ }
+ var stockRefreshSequence=0;
+ async function refreshStock(done) {
+ var sequence=++stockRefreshSequence, branch=String(PosnicPro.local?.get('branch_id_set')||'');
PosnicPro.sales.itemCache?.clear();
- var branch=String(PosnicPro.local?.get('branch_id_set')||'');
- PosnicPro.sales.loadBillingCatalogue(function(response){
- if(branch!==String(PosnicPro.local?.get('branch_id_set')||'') || !active()){done?.(false);return;}
- if(response?.type!=='success'){done?.(false);PosnicPro.alert('error', PosnicPro.i18n.t('lang_stock_display_could_not_refresh_refresh_sa', 'Stock display could not refresh. Refresh Sales before checking availability.'));return;}
- var items=new Map(),families={},pending=1,ok=true;
- function settled(){if(--pending===0)done?.(ok);}
- response.data.forEach(function(item){items.set(String(item.id||item.item_id),item);if(item.variant_group_id)(families[item.variant_group_id]||(families[item.variant_group_id]=[])).push(item);});
- if(PosnicPro.sales.itemsMenu)PosnicPro.sales.itemsMenu._families=families;
- document.querySelectorAll('#sales_new .wsk-cp').forEach(function(tile){
- var rows=tile.dataset.variantGroup?families[tile.dataset.variantGroup]:[items.get(tile.id)];
- if(!rows || !rows[0]){
- var missingStock=tile.querySelector('.wsk-cp-stock');
- if(missingStock && /^[a-f0-9]{24}$/i.test(tile.id) && PosnicPro.get){
- pending++;missingStock.textContent='Updating stock…';
- function failed(){ok=false;if(tile.isConnected)missingStock.textContent='Stock unavailable';settled();}
- PosnicPro.get('items/'+tile.id,function(response){
- if(branch!==String(PosnicPro.local?.get('branch_id_set')||'') || !tile.isConnected){ok=false;settled();return;}
- if(response?.type!=='success'){failed();return;}
- var quantity=Number(response.data?.available_quantity);
- missingStock.textContent=Number.isFinite(quantity)?quantity+' in stock':'Stock unavailable';
- if(!Number.isFinite(quantity))ok=false;settled();
- },failed);
- }
- return;
- }
- var tracked=rows.filter(function(r){return r.track_inventory===true || r.track_inventory==='true';}),stock=tile.querySelector('.wsk-cp-stock');
- if(stock)stock.textContent=tracked.length?tracked.reduce(function(sum,r){return sum+(Number(r.available_quantity)||0);},0)+' in stock':'';
+ var families=PosnicPro.sales.itemsMenu?._families || {};
+ var targets=Array.from(document.querySelectorAll('#sales_new .wsk-cp')).map(function(tile){
+ var rows=tile.dataset.variantGroup?families[tile.dataset.variantGroup]||[]:[];
+ return {tile:tile,stock:tile.querySelector('.wsk-cp-stock'),ids:tile.dataset.variantGroup?rows.map(function(r){return String(r.id||r.item_id);}):[tile.id]};
+ }).filter(function(t){return t.stock && t.ids.length && t.ids.every(function(id){return /^[a-f0-9]{24}$/i.test(id);});});
+ targets.forEach(function(t){t.stock.textContent='Updating stock...';});
+ var ok=true;
+ try {
+ var items=await freshProducts(targets.flatMap(function(t){return t.ids;}));
+ if(sequence!==stockRefreshSequence || branch!==String(PosnicPro.local?.get('branch_id_set')||'') || !active()){done?.(false);return;}
+ targets.forEach(function(t){
+ if(!t.tile.isConnected)return;
+ var rows=t.ids.map(function(id){return items.get(id);});
+ if(rows.some(function(r){return !r || !Number.isFinite(Number(r.available_quantity));})) {t.stock.textContent='Stock unavailable';ok=false;return;}
+ var tracked=rows.filter(function(r){return r.track_inventory===true || r.track_inventory==='true';});
+ t.stock.textContent=tracked.length?tracked.reduce(function(sum,r){return sum+Number(r.available_quantity);},0)+' in stock':'';
+ if(t.tile.dataset.variantGroup) families[t.tile.dataset.variantGroup]=rows.map(function(r,i){return Object.assign({},r,{id:t.ids[i]});});
});
- settled();
- },true);
+ } catch(error) {
+ ok=false;
+ if(sequence===stockRefreshSequence)targets.forEach(function(t){if(t.tile.isConnected)t.stock.textContent='Stock unavailable';});
+ }
+ if(!ok)PosnicPro.alert('error',PosnicPro.i18n.t('lang_stock_display_could_not_refresh_refresh_sa','Stock display could not refresh. Refresh Sales before checking availability.'));
+ done?.(ok);
}
function confirmClear() {
if(document.getElementById('sale-clear-dialog') || dialog || !cart().length)return;
@@ -200,10 +209,9 @@
if(input.context && JSON.stringify(input.context).length>1024)throw Error('Invalid sale context.');
restoring=true;
try{
- var response=lines.length?await new Promise(function(resolve){PosnicPro.sales.loadBillingCatalogue(resolve,true);}):{type:'success',data:[]};
- if(response.type!=='success')throw Error('Products could not be loaded. Try again.');
+ var products=await freshProducts(lines.map(function(line){return line.productId;}));
if(branch!==String(PosnicPro.local.get('branch_id_set')||'') || (lines.length && cart().length) || selected?.id!==id)throw Error('The shop or current sale changed. Try again.');
- var items=lines.map(function(line){var item=response.data.find(function(p){return String(p.id||p.item_id)===line.productId;});if(!item)throw Error('A saved product is unavailable. Review the basket before resuming.');return Object.assign({},item,{available_quantity:Number(item.available_quantity)||0,id:line.productId,item_quantity:line.quantityMilli/1000,selling_price:line.sellingPrice,mrp_price:line.sellingPrice,discount_amount:0,discount_percentage:0,_modifiersResolved:true,_priceAsked:true});});
+ var items=lines.map(function(line){var item=products.get(line.productId);if(!item)throw Error('A saved product is unavailable. Review the basket before resuming.');return Object.assign({},item,{available_quantity:Number(item.available_quantity)||0,id:line.productId,item_quantity:line.quantityMilli/1000,selling_price:line.sellingPrice,mrp_price:line.sellingPrice,discount_amount:0,discount_percentage:0,_modifiersResolved:true,_priceAsked:true});});
if(!/^#\/?sales\/new(?:$|\?)/.test(location.hash)){
location.hash='#/sales/new';
await new Promise(function(resolve,reject){var until=Date.now()+8000;function ready(){var scanner=document.getElementById('sales_new_item_name');if(scanner?.getClientRects().length && PosnicPro.sales.SaleAction==='add')return resolve();if(Date.now()>until)return reject(Error('Open Sales and try resuming again.'));setTimeout(ready,50);}setTimeout(ready,50);});
@@ -226,4 +234,3 @@
})();
-
diff --git a/tests/sale-extension-stock-refresh.test.js b/tests/sale-extension-stock-refresh.test.js
new file mode 100644
index 000000000..49922fda7
--- /dev/null
+++ b/tests/sale-extension-stock-refresh.test.js
@@ -0,0 +1,49 @@
+'use strict';
+const test=require('node:test');
+const assert=require('node:assert/strict');
+const fs=require('node:fs');
+const {JSDOM}=require('jsdom');
+const source=fs.readFileSync(require.resolve('../frontend/static/script/js/modules/js/sale-extensions.js'),'utf8');
+const id=n=>String(n).padStart(24,'0');
+function setup(count=1){
+ const dom=new JSDOM(''+Array.from({length:count},(_,i)=>` 23 in stock `).join('')+' ');
+ let branch='shop',clears=0;const requests=[],alerts=[];
+ const PosnicPro={i18n:{t:(_key,text)=>text},local:{get:()=>branch},sales:{itemCache:{clear(){clears++;}},itemsMenu:{_families:{}}},get(url,ok,fail){requests.push({url,ok,fail});},alert(...args){alerts.push(args);}};
+ const code=source.slice(source.indexOf(' async function freshProducts'),source.indexOf(' function confirmClear'));
+ const api=new Function('PosnicPro','document','active',code+'return {refreshStock,freshProducts};')(PosnicPro,dom.window.document,()=>true);
+ return {...api,dom,PosnicPro,requests,alerts,setBranch:b=>branch=b,get clears(){return clears;},stock:()=>dom.window.document.querySelector('.wsk-cp-stock').textContent};
+}
+test('refresh fetches visible stock without the removed catalogue loader',async()=>{
+ const s=setup();const p=s.refreshStock();assert.equal(s.clears,1);assert.equal(s.stock(),'Updating stock...');
+ assert.equal(s.requests[0].url,'items/'+id(1));s.requests[0].ok({type:'success',data:{available_quantity:16,track_inventory:true}});await p;
+ assert.equal(s.stock(),'16 in stock');assert.equal(s.requests.length,1);
+});
+test('new refresh wins over an older response',async()=>{
+ const s=setup();const first=s.refreshStock(),second=s.refreshStock();
+ s.requests[1].ok({type:'success',data:{available_quantity:15,track_inventory:true}});await second;
+ s.requests[0].ok({type:'success',data:{available_quantity:23,track_inventory:true}});await first;assert.equal(s.stock(),'15 in stock');
+});
+test('refresh failure is visible and never reports success',async()=>{
+ const s=setup();let result;const p=s.refreshStock(ok=>result=ok);s.requests[0].fail();await p;
+ assert.equal(result,false);assert.equal(s.stock(),'Stock unavailable');assert.equal(s.alerts.length,1);
+});
+test('branch change and removed tiles cannot receive stale updates',async()=>{
+ const s=setup();const p=s.refreshStock();s.setBranch('other');s.requests[0].ok({type:'success',data:{available_quantity:16,track_inventory:true}});await p;
+ assert.notEqual(s.stock(),'16 in stock');
+});
+test('fresh reads deduplicate ids, bound concurrency, and reach saved off-page products',async()=>{
+ const s=setup(0);const p=s.freshProducts([...Array.from({length:8},(_,i)=>id(i+1)),id(8)]);
+ assert.equal(s.requests.length,6);
+ for(let i=0;i<8;i++){s.requests[i].ok({type:'success',data:{id:id(i+1),available_quantity:i}});await new Promise(setImmediate);}
+ assert.equal((await p).size,8);assert.equal(s.requests.length,8);
+});
+test('variant family stock and picker rows are both refreshed',async()=>{
+ const s=setup();const tile=s.dom.window.document.querySelector('.wsk-cp');tile.dataset.variantGroup='family';
+ s.PosnicPro.sales.itemsMenu._families.family=[{id:id(1)},{id:id(2)}];
+ const p=s.refreshStock();s.requests[0].ok({type:'success',data:{available_quantity:2,track_inventory:true}});s.requests[1].ok({type:'success',data:{available_quantity:3,track_inventory:true}});await p;
+ assert.equal(s.stock(),'5 in stock');assert.equal(s.PosnicPro.sales.itemsMenu._families.family[1].available_quantity,3);
+});
+test('saved basket restore no longer depends on full catalogue loading',()=>{
+ assert.ok(!source.includes('loadBillingCatalogue'));
+ assert.ok(source.includes('var products=await freshProducts(lines.map'));
+});
|