import {mountAppControls} from './app-mode.js?v=1'; import {mountLogo} from './ds/logo.js'; import {mountMobileChrome} from './ds/frame.js'; import {npubFrom} from './ds/env.js'; const app=document.querySelector('#app');const params=new URLSearchParams(location.search);const npub=npubFrom();let merchant,state,timer,busy=false,lastIds='',lastBookingIds=''; const esc=v=>String(v??'').replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); const ago=stamp=>{const s=Math.max(0,Math.floor((Date.now()-new Date(stamp).getTime())/1000));return s<60?`${s}s`:s<3600?`${Math.floor(s/60)}m`:`${Math.floor(s/3600)}h`}; function error(message){app.innerHTML=`
⌁

Just Works POS

${esc(message)}

Use pos.justworks.cash/?npub=npub1…
`} function shell(){app.innerHTML=`
Just WorksCounter POS
${esc(merchant.name)}${esc([merchant.category,merchant.area].filter(Boolean).join(' · '))}
Live
Waiting0
Preparing0
Ready0
Payments0
`;document.querySelectorAll('[data-tab]').forEach(b=>b.onclick=()=>showTab(b.dataset.tab));document.querySelectorAll('[data-logo]').forEach(host=>mountLogo(host,{icon:true}));mountMobileChrome(app);mountAppControls(document.querySelector(".pos-app-controls"));if(location.hash==='#bookings')showTab('bookings');tick();setInterval(tick,10000)} function tick(){const c=document.querySelector('#clock');if(c)c.textContent=new Date().toLocaleTimeString([],{hour:'2-digit',minute:'2-digit'})} function showTab(id){document.querySelectorAll('[data-tab]').forEach(b=>b.classList.toggle('active',b.dataset.tab===id));document.querySelectorAll('.board').forEach(b=>b.classList.toggle('active',b.id===id))} function orderCard(order){const next={new:['Accept','accepted'],accepted:['Mark delivered','delivered'],delivered:['Close table','closed']}[order.status];const request=order.items.map(item=>`${item.qty>1?`${item.qty}× `:''}${esc(item.name)}`).join(' · ');return `
${order.table_number}
Table${order.table_number}

${request}

${order.note?`${esc(order.note)}`:''}
`} function paymentCard(payment){return `
€
Table ${payment.table_number}${payment.amount_sats.toLocaleString()} sats

€${(payment.amount_cents/100).toFixed(2)} · ${payment.status}

${payment.status==='paid'?``:'Waiting for Lightning settlement…'}
`} function bookingCard(booking){const next={new:['Confirm booking','confirmed'],confirmed:['Mark seated','seated'],seated:['Close','closed']}[booking.status];const time=new Date(booking.scheduled_at);return `
${booking.guests}
${booking.mode==='now'?'Arriving soon':'Reserved'}${time.toLocaleDateString([],{weekday:'short',day:'numeric',month:'short'})} · ${time.toLocaleTimeString([],{hour:'2-digit',minute:'2-digit'})}

${esc(booking.guest_name)} · ${booking.guests} ${booking.guests===1?'guest':'guests'}

${booking.notes?`${esc(booking.notes)}`:''}
`} function render(){const orders=state.orders||[],payments=state.payments||[],bookings=state.bookings||[];document.querySelector('#waitingCount').textContent=orders.filter(o=>o.status==='new').length;document.querySelector('#preparingCount').textContent=orders.filter(o=>o.status==='accepted').length;document.querySelector('#readyCount').textContent=orders.filter(o=>o.status==='delivered').length;document.querySelector('#paymentCount').textContent=payments.length;document.querySelector('#orderBadge').textContent=orders.length;document.querySelector('#payBadge').textContent=payments.length;document.querySelector('#bookingBadge').textContent=bookings.length;document.querySelector('#orders').innerHTML=orders.length?orders.map(orderCard).join(''):`
✓

No open orders.

New table orders appear here automatically.

`;document.querySelector('#payments').innerHTML=payments.length?payments.map(paymentCard).join(''):`
€

No pending payments.

Pay Bill and Just Pay confirmations appear here.

`;document.querySelector('#bookings').innerHTML=bookings.length?bookings.map(bookingCard).join(''):`
□

No upcoming bookings.

Table reservations appear here automatically.

`;document.querySelectorAll('[data-order]').forEach(b=>b.onclick=()=>updateOrder(b.dataset.order,b.dataset.status));document.querySelectorAll('[data-payment]').forEach(b=>b.onclick=()=>ack(b.dataset.payment));document.querySelectorAll('[data-booking]').forEach(b=>b.onclick=()=>updateBooking(b.dataset.booking,b.dataset.status));const ids=orders.map(o=>o.id).join(',');if(lastIds&&ids!==lastIds&&orders.some(o=>!lastIds.includes(o.id)))notify('New order received');lastIds=ids;const bookingIds=bookings.map(item=>item.id).join(',');if(lastBookingIds&&bookingIds!==lastBookingIds&&bookings.some(item=>!lastBookingIds.includes(item.id)))notify('New booking received');lastBookingIds=bookingIds} function notify(text){const t=document.querySelector('#toast');t.textContent=text;t.classList.add('show');setTimeout(()=>t.classList.remove('show'),1800)} async function updateOrder(id,status){if(busy)return;busy=true;try{const r=await fetch(`/api/orders/${id}/status`,{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({status})});if(!r.ok)throw new Error();notify(status==='accepted'?'Order accepted':status==='delivered'?'Marked delivered':'Table closed');await load()}catch{notify('Could not update order')}finally{busy=false}} async function ack(id){if(busy)return;busy=true;try{const response=await fetch(`/api/payments/${id}/acknowledge`,{method:'POST'});if(!response.ok)throw new Error();notify('Payment cleared');await load()}catch{notify('Could not acknowledge payment. Check your login.')}finally{busy=false}} async function updateBooking(id,status){if(busy)return;busy=true;try{const response=await fetch(`/api/bookings/${id}/status`,{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({status})});if(!response.ok)throw new Error();notify(status==='confirmed'?'Booking confirmed':status==='seated'?'Guests seated':'Booking closed');await load()}catch{notify('Could not update booking')}finally{busy=false}} function unavailable(message){const connection=document.querySelector('#connection');connection?.classList.add('offline');connection?.querySelector('span').replaceChildren(message);if(!document.querySelector('#orders'))return;state={orders:[],payments:[],bookings:[]};render();document.querySelector('#orders').innerHTML=message==='Login required'?'

Merchant login required

Use Login above to connect your Business session.

':'

Waiting for connection

Live orders and payments are unavailable. Nothing is saved for later.

'} window.addEventListener('offline',()=>unavailable('Offline')); async function load(){const connection=document.querySelector('#connection');try{if(!navigator.onLine)throw new Error('Offline');const response=await fetch(`/api/pos?npub=${encodeURIComponent(npub)}`,{cache:'no-store'});if(!response.ok)throw new Error(response.status===403?'Login required':'Reconnecting');const data=await response.json();if(!navigator.onLine)throw new Error('Offline');state=data;render();connection?.classList.remove('offline');connection?.querySelector('span').replaceChildren('Live')}catch(error){unavailable(error.message==='Login required'?'Login required':navigator.onLine?'Reconnecting':'Offline')}} async function boot(){if(!npub)return error('Add the merchant npub to start.');const data=await fetch(`/api/order-merchant?npub=${encodeURIComponent(npub)}`,{cache:'no-store'}).then(async r=>{const d=await r.json();if(!r.ok)throw new Error(d.error);return d});merchant=data.merchant;shell();tick();await load();timer=setInterval(load,1500)}boot().catch(e=>error(e.message||'Merchant unavailable'));