// Customer login has no relationship to the protected staff workspace. var CustomerLogin = { create(options){ const overlay=document.getElementById('customer-login'); const form=document.getElementById('customer-login-form'); const username=document.getElementById('customer-username'); const password=document.getElementById('customer-password'); const submit=document.getElementById('customer-submit'); const cancel=document.getElementById('customer-cancel'); const error=document.getElementById('customer-error'); const accounts=['customer-account','customer-account-mobile'].map(id=>document.getElementById(id)).filter(Boolean); let user, pending=null, opener=null, busy=false, version=0, resumePending=false; overlay.hidden=true; function status(message){ error.textContent=message || ''; error.hidden=!message; } function render(){ accounts.forEach(function(account){ account.textContent=user ? user.name+' ยท '+options.text('customerLogout') : options.text('customerAccount'); account.title=account.textContent; account.setAttribute('aria-label',account.textContent); }); submit.textContent=options.text(busy ? 'customerSigningIn' : 'customerLogin'); cancel.textContent=options.text('customerCancel'); document.getElementById('customer-login-title').textContent=options.text('customerLogin'); document.getElementById('customer-username-label').textContent=options.text('customerUsername'); document.getElementById('customer-password-label').textContent=options.text('customerPassword'); username.placeholder=options.text('customerUsernamePlaceholder'); password.placeholder=options.text('customerPasswordPlaceholder'); } function update(next, refreshOrders){ const previous=user; user=next; render(); if(options.onChange && (previous === undefined || (previous && previous.id) !== (user && user.id))){ options.onChange(user,previous); } if(options.onSession) options.onSession(user,Boolean(refreshOrders)); } async function request(path, data){ const controller=new AbortController(); const timer=setTimeout(()=>controller.abort(),10000); try{ const response=await options.fetch(path,{method:data === undefined ? 'GET' : 'POST', headers:{'Content-Type':'application/json'},body:data === undefined ? undefined : JSON.stringify(data),signal:controller.signal}); const result=await response.json(); if(!response.ok || !result.ok){ const failure=new Error(result.error || result.e || options.text('customerLoginFailed')); failure.publicMessage=true;throw failure; } return result; }finally{ clearTimeout(timer); } } async function refresh(refreshOrders){ if(busy) return; const token=++version; try{ const result=await request('/api/customer/session'); if(token === version) update(result.authenticated && result.user ? result.user : null,refreshOrders); }catch(error){} // Query endpoints still enforce login if status cannot refresh. } function restoreFocus(){ if(opener && opener.isConnected && !opener.disabled){ opener.focus({preventScroll:true}); opener=null; } } function close(){ overlay.hidden=true; password.value=''; pending=null; restoreFocus(); } function open(onSuccess){ if(busy) return; pending=typeof onSuccess === 'function' ? onSuccess : null; opener=document.activeElement; status(''); password.value=''; overlay.hidden=false; if(options.onOpen) options.onOpen(); render(); username.focus({preventScroll:true}); } function setBusy(value){ busy=value; submit.disabled=value; cancel.disabled=value; accounts.forEach(account=>{account.disabled=value;}); username.disabled=value; password.disabled=value; render(); if(!busy && overlay.hidden) restoreFocus(); } form.addEventListener('submit',async function(event){ event.preventDefault(); if(busy) return; const token=++version; setBusy(true);status(''); try{ const result=await request('/api/customer/login',{username:username.value,password:password.value}); if(token !== version) return; update(result.user); const resume=pending;close(); if(resume) await resume(); }catch(failure){ if(token === version) status(failure.publicMessage ? failure.message : options.text('customerLoginFailed')); }finally{ if(token === version) setBusy(false); } }); cancel.onclick=function(){ if(!busy) close(); }; overlay.addEventListener('click',function(event){ if(event.target === overlay && !busy) close(); }); overlay.addEventListener('keydown',function(event){ if(event.key === 'Escape' && !busy){ event.preventDefault();close(); } if(event.key === 'Tab'){ const controls=[username,password,submit,cancel].filter(control=>!control.disabled); if(!controls.length){ event.preventDefault();return; } const index=controls.indexOf(document.activeElement); const next=(index+(event.shiftKey ? -1 : 1)+controls.length)%controls.length; event.preventDefault();controls[next].focus({preventScroll:true}); } }); async function activateAccount(){ if(busy) return; if(!user){ open(); return; } const token=++version;setBusy(true); try{ await request('/api/customer/logout',{}); if(token === version) update(null); }catch(failure){ if(options.onError) options.onError(options.text('customerLogoutFailed')); } finally{ if(token === version) setBusy(false); } } accounts.forEach(account=>{account.onclick=activateAccount;}); async function resume(){ // Browsers often emit both events for one return to the page. if(document.hidden || resumePending) return; resumePending=true; try{ await refresh(true); } finally{ resumePending=false; } } window.addEventListener('focus',resume); document.addEventListener('visibilitychange',resume); render(); return {open:open,refresh:refresh,render:render,require:async function(resume){ if(busy) return; await refresh(); if(user){ await resume(); }else open(resume); },viewport:function(height,top){ overlay.style.height=height || ''; overlay.style.top=top || ''; overlay.style.bottom=height ? 'auto' : ''; }}; } };