Ny ordre #—
🍽 Dine-In · Mat 15%
Tom ordre — velg varer fra menyen
Ordrer
Aktive, hold og innkommende bestillinger
Bord & områder
Plantegning · dra for å flytte bord
👆 Klikk for å legge til bord  ·  ✋ Dra for å flytte
Ledig Opptatt Reservert 0 bord · 0 stoler
Bord (0)
Soner
Retter
Alle retter i menyen
🔡 Tekststørrelser på varekort
Emoji
Navn
Pris
Beskrivelse
Statistikk
Fisketorget Bergen
Totalt salg
32 450 kr
↑ 14% vs i går
Ordrer i dag
147
↑ 8% vs i går
Snittordre
221 kr
Eks. MVA: 185 kr
MVA skyldig
4 867 kr
15%: 2 489 · 25%: 2 378
Salg per modus
🍽 Dine-In16 874 kr
🥡 Takeaway11 033 kr
🛵 Levering4 543 kr
52%
Dine-In
34%
Takeaway
14%
Levering
🏆 Bestselgere i dag
🥤
Cola
Drikkevarer
67
solgt
🍲
Fiskesuppe
Forretter
43
solgt
🐟
Laks
Hovedretter
35
solgt
🍔
Burger
Hovedretter
28
solgt
💳 Betalingsmetoder
💳 Kort18 340 kr
📱 Vipps9 210 kr
💵 Kontant4 900 kr
🧾 MVA-oversikt
MVA 25% (drikke/alkohol)
Grunnlag: 9 512 kr
2 378 kr
MVA 15% (mat, dine-in)
Grunnlag: 16 594 kr
2 124 kr
MVA 12% (mat, take/lev)
Grunnlag: 3 054 kr
366 kr
Total MVA skyldig4 868 kr
Innstillinger
Generelt
🖨️
Skrivere
📱
Enheter
📡
NFC
👥
Brukere
Kasse
💳
Betalinger
🔄
Kassaoppgjør
📊
Rapport
🗄️
Åpne kasseskuffe
🎁
Gavekort
🧾
Regnskap/MVA
👤
Kunder
Integrasjoner
💜
Vipps
🏦
Nets
📦
Wolt
🟡
Foodora
Kvitteringsmal
HTML-basert utskrift · Inter-font · 58mm / 80mm
📄 Forhåndsvisning
📤 Last opp logo
Klikk her
Fisketorget Bergen
Order details:
Number:1
Placed at:21 Apr 10:11
Accepted at:21 Apr 10:12
Client info:
First name:Sajid
Last name:Dad
Email:test@eat-now.no
Phone:+47 66 84 57 05
Items:
2x Fiskesuppe370.00
1x Laks295.00
2x Cola 0,33L110.00
Sub-total:NOK 775.00
MVA (15%):NOK 101.09
Total:NOK 775.00
Payment:💳 Kort
Din egendefinerte tekst her
Seksjoner & font-størrelser
Restaurantnavn
Bestillingsdetaljer
Number, dato, tidspunkt
Client info
Navn, e-post, telefon
Items / Varer
Sub-total & Total
MVA-linje
MVA (15%): NOK xxx
Betalingsmetode
Bunn (adresse, tlf)
Egendefinert infoboks
Egendefinert tekst
Statistikk
Fisketorget Bergen
Totalt salg
32 450 kr
↑ 14% vs i går
Ordrer i dag
147
↑ 8% vs i går
Snittordre
221 kr
Eks. MVA: 185 kr
MVA skyldig
4 867 kr
15%: 2 489 · 25%: 2 378
Salg per modus
🍽 Dine-In16 874 kr
🥡 Takeaway11 033 kr
🛵 Levering4 543 kr
52%
Dine-In
34%
Takeaway
14%
Levering
🏆 Bestselgere i dag
🥤
Cola
Drikkevarer
67
solgt
🍲
Fiskesuppe
Forretter
43
solgt
🐟
Laks
Hovedretter
35
solgt
🍔
Burger
Hovedretter
28
solgt
💳 Betalingsmetoder
💳 Kort18 340 kr
📱 Vipps9 210 kr
💵 Kontant4 900 kr
🧾 MVA-oversikt
MVA 25% (drikke/alkohol)
Grunnlag: 9 512 kr
2 378 kr
MVA 15% (mat, dine-in)
Grunnlag: 16 594 kr
2 124 kr
MVA 12% (mat, take/lev)
Grunnlag: 3 054 kr
366 kr
Total MVA skyldig4 868 kr
Innstillinger
Generelt
🖨️
Skrivere
📱
Enheter
📡
NFC
👥
Brukere
Kasse
💳
Betalinger
🔄
Kassaoppgjør
📊
Rapport
🗄️
Åpne kasseskuffe
🎁
Gavekort
🧾
Regnskap/MVA
👤
Kunder
Integrasjoner
💜
Vipps
🏦
Nets
📦
Wolt
🟡
Foodora
Kvitteringsmal
HTML-basert utskrift · Inter-font · tilpass alt
📄 Forhåndsvisning
📤 Last opp logo
Klikk her
Fisketorget Bergen
Order details:
Number:1
Placed at:21 April at 10:11
Accepted at:21 April at 10:12
Fulfillment at:21 April at 11:12
Client info:
First name:Sajid
Last name:Dad
Email:test@eat-now.no
Phone:+47 66 84 57 05
Items:
2x Fiskesuppe370.00
Standard · MVA 15%
1x Laks295.00
2x Cola 0,33L110.00
Sub-total:NOK 775.00
MVA (15% included):NOK 101.09
Total:NOK 775.00
Paid
Not Paid
Payment:💳 Kortbetaling
Din egendefinerte tekst her
Seksjoner & font-størrelser
Restaurantnavn
Bestillingsdetaljer
Number, dato, tidspunkt
Client info
Navn, e-post, telefon
Items / Varer
Sub-total & Total
MVA-linje
MVA (15% included): NOK xxx
Paid / Not Paid boks
Betalingsmetode
Bunn (adresse, tlf)
Egendefinert infoboks
Egendefinert tekst
Kjøkken
Alle ordrer inn fra POS — oppdater status
Auto-oppdaterer hvert 15s
System
Konfigurasjon og innstillinger

🏪 Informasjon om restaurant

Grunnleggende informasjon som vises på kvitteringer og i systemet.

📋 Bilagsnummer-serie

---
Neste bilagsnummer som tildeles ved ny ordre. Serien kan ikke slettes eller tilbakestilles etter oppstart.

📜 Bilagsarkiv

Alle bilag er permanent arkivert. Ingen kan slettes.

🏢 Portal Logo (Super Admin)

Logoen vises øverst i sidemenyen. Kun Super Admin kan endre denne.
🏢 Last opp portal-logo
PNG, SVG eller JPG · Anbefalt: 300×80px

⏱️ Kjøkken-timer innstillinger

Automatisk nedtelling fra betaling til "Ferdig". Sett 0 for å deaktivere automatisk timer.
💡 Status settes automatisk til Pågår ved betaling. Timer teller ned og setter Ferdig automatisk.
Brukere & roller
Administrer ansatte, PIN-koder og tilganger
👤 Kasserer
POS · Bord · Kjøkken · Ordrer · Menyer (vis)
🛡 Admin
Alt Kasserer + Menyer (rediger) · System · Oppgjør · Brukere
👁 Kjøkken
Kun Kjøkken-visning · Ingen kasse-tilgang
Ansatte (0)
NavnInitialerPINRolleStatus
📋 Innloggingslogg (siste 10)
Ingen innlogginger registrert ennå.
Rapporter
Kassaoppgjør, X/Z-rapport og SAF-T eksport
💳 Betalingsmetoder
🧾 MVA-spesifikasjon
Bilagslogg
`);w.document.close();setTimeout(()=>w.print(),500);} else toast('⚠ Tillat popups for forhåndsvisning'); } function removeLogo(type){ LOGOS[type]=null; applyLogos(); toast('Logo fjernet'); } function applyLogos(){ // ── FULL SIDEBAR ── const sbLogo=el('sbPortalLogo'),sbText=el('sbLogoText'); const prevFull=el('logoPreviewFull'),phFull=el('logoPHFull'); if(LOGOS.full){ if(sbLogo){sbLogo.src=LOGOS.full;sbLogo.style.display='block';} if(sbText)sbText.style.display='none'; if(prevFull){prevFull.src=LOGOS.full;prevFull.style.display='block';} if(phFull)phFull.style.display='none'; } else { if(sbLogo)sbLogo.style.display='none'; if(sbText)sbText.style.display=''; if(prevFull){prevFull.src='';prevFull.style.display='none';} if(phFull)phFull.style.display='block'; } // ── COLLAPSED LOGO ── const colLogoEl=el('sbLogoCol'); const prevCol=el('logoPreviewCol'),phCol=el('logoPHCol'); if(LOGOS.col){ if(colLogoEl){ colLogoEl.innerHTML=``; } if(prevCol){prevCol.src=LOGOS.col;prevCol.style.display='block';} if(phCol)phCol.style.display='none'; } else { if(colLogoEl)colLogoEl.innerHTML='SO'; if(prevCol){prevCol.src='';prevCol.style.display='none';} if(phCol)phCol.style.display='block'; } // ── LOGIN LOGO ── const loginImg=el('loginLogoImg'),loginText=el('loginLogoText'); const prevLogin=el('logoPreviewLogin'),phLogin=el('logoPHLogin'); if(LOGOS.login){ if(loginImg){loginImg.src=LOGOS.login;loginImg.style.display='block';} if(loginText)loginText.style.display='none'; if(prevLogin){prevLogin.src=LOGOS.login;prevLogin.style.display='block';} if(phLogin)phLogin.style.display='none'; } else { if(loginImg){loginImg.src='';loginImg.style.display='none';} if(loginText)loginText.style.display=''; if(prevLogin){prevLogin.src='';prevLogin.style.display='none';} if(phLogin)phLogin.style.display='block'; } } /* ── BILAG

🏢 Logo-administrasjon (Super Admin)

Kun Super Admin kan endre logoer. Endringer vises umiddelbart i systemet.
Utvidet sidebar
Vises øverst i sidemenyen
📤 Last opp
Anbefalt: 200×50px
Minimert sidebar
Lite ikon i minimert modus
📤 Last opp
Anbefalt: 64×64px
Innloggingsskjerm
Logo på PIN-skjermen
📤 Last opp
Anbefalt: 200×60px
/* ═══ BILAGSNUMMER SYSTEM ═══ */ let bilagSeries={active:false,prefix:'FK',next:1001,step:1,count:0}; let bilagArchiveArr=[]; let REFUNDS=[]; // Refusjoner function getBilagNum(){ if(!bilagSeries.active)return null; const num=bilagSeries.prefix+'-'+bilagSeries.next; bilagSeries.next+=bilagSeries.step;bilagSeries.count++; bilagArchiveArr.push({num,time:new Date(),type:posOT,total:calcT().t}); return num; } function activateBilag(){ const prefix=el('bilagPrefix').value.trim()||'ORD'; const start=parseInt(el('bilagStart').value)||1001; const step=parseInt(el('bilagStep').value)||1; if(bilagSeries.active){toast('⚠ Serien er allerede aktiv og låst');return;} bilagSeries={active:true,prefix,next:start,step,count:0}; el('bilagSetupArea').style.display='none';el('bilagActiveArea').style.display='block'; el('bilagPrefixShow').textContent=prefix;el('bilagNextShow').textContent=prefix+'-'+start; updateBilagDisplay();toast('✓ Nummerserie aktivert: '+prefix+'-'+start); } function updateBilagDisplay(){ el('bilagDisplay').textContent=bilagSeries.active?(bilagSeries.prefix+'-'+bilagSeries.next):'Ikke konfigurert'; if(bilagSeries.active){el('bilagNextShow').textContent=bilagSeries.prefix+'-'+bilagSeries.next;el('bilagCountShow').textContent=bilagSeries.count;} } function buildAdmin(){ updateBilagDisplay(); // Archive table const arc=el('bilagArchive'); if(!bilagArchiveArr.length){arc.innerHTML='
Ingen bilag utstedt ennå
';return;} arc.innerHTML=`${bilagArchiveArr.slice().reverse().map(b=>``).join('')}
Bilagsnr.TidspunktTypeBeløp
${b.num}${b.time.toLocaleString('nb-NO')}${b.type}${nok(b.total)}
`; } /* ═══ AUTO BILAG ON ORDER START ═══ */ /* clearOrder bilag reset handled in original function */ /* ═══ KJØKKEN SYSTEM ═══ */ let KITCHEN_ORDERS=[]; let kjkFilter='all'; // When finishPay is called, also push to kitchen /* finishPay kitchen push handled in original function */ // holdOrder uses the main function directly function buildKjokken(){ updKjkBadge(); let ords=KITCHEN_ORDERS; if(kjkFilter!=='all')ords=ords.filter(o=>o.status===kjkFilter); ords=[...ords].sort((a,b)=>{ const rank={Venter:0,'Pågår':1,Fullført:2,Kansellert:3}; return rank[a.status]-rank[b.status]||(a.time-b.time); }); if(!ords.length){ el('kjkGrid').innerHTML=`
Ingen ordrer ${kjkFilter==='all'?'':'med status '+kjkFilter}
`; return; } el('kjkGrid').innerHTML=ords.map(o=>{ const elapsed=Math.floor((Date.now()-o.time)/60000); const timerSecs=o.timerEnd?Math.max(0,Math.ceil((o.timerEnd-Date.now())/1000)):null; const timerMin=timerSecs!==null?Math.floor(timerSecs/60):null; const timerSec=timerSecs!==null?timerSecs%60:null; const timerStr=timerSecs!==null?(timerMin+':'+(timerSec<10?'0':'')+timerSec):null; const timerDone=timerSecs===0; const timerColor=timerSecs!==null&&timerSecs<60?'var(--ac2)':timerSecs!==null&&timerSecs<120?'var(--wa)':'var(--gr)'; return`
${o.bilag}
${o.type}${o.bord?' · Bord '+o.bord.num:''}
${o.status} ${(timerStr&&(o.status==='Pågår'||o.status==='Hold'))?`
${timerStr}
`:`
${elapsed<1?'Nå nettopp':elapsed+'min siden'}
`}
${o.cust&&o.cust.id?`
👤 ${o.cust.name}${o.cust.phone?' · '+o.cust.phone:''}
`:''}
${o.items.map(it=>`
${it.qty}×
${it.e||'🍽️'} ${it.name}${it.size&&it.size!=='Standard'?` (${it.size})`:''}
${it.note?`
📝 ${it.note}
`:''}
`).join('')}
Total: ${nok(o.total)} ${o.time.toLocaleTimeString('nb-NO',{hour:'2-digit',minute:'2-digit'})}
`; }).join(''); } function filtKjk(f,btn){ kjkFilter=f; all('.kjk-filter',b=>b.classList.remove('on'));btn.classList.add('on'); buildKjokken(); } function setKjkStatus(id, status){ const o=KITCHEN_ORDERS.find(x=>x.id===id); if(!o)return; const prev=o.status; o.status=status; // If Pågår: start/restart timer if(status==='Pågår'){ const mins=getTimerMins(o.type); if(mins>0){ o.timerEnd=Date.now()+(mins*60*1000); o.timerId&&clearTimeout(o.timerId); o.timerId=setTimeout(()=>{ const fresh=KITCHEN_ORDERS.find(x=>x.id===id); if(fresh&&fresh.status==='Pågår'){ setKjkStatus(id,'Fullført'); } }, mins*60*1000); } } // If Venter: cancel timer countdown if(status==='Venter'){ o.timerEnd=null; o.timerId&&clearTimeout(o.timerId); } // If Fullført or Kansellert: move to ORDERS, remove from kitchen if(status==='Fullført'||status==='Kansellert'){ o.timerId&&clearTimeout(o.timerId); // Add to ORDERS list const existing=ORDERS.find(x=>x.bilag===o.bilag); if(!existing){ ORDERS.push({ id:ordCtr++,num:o.bilag,type:o.type, status:status==='Fullført'?'fullfort':'kansellert', items:o.items,total:o.total,cust:o.cust,bord:o.bord, time:o.time,method:o.method,bilag:o.bilag }); updOrdBadge(); } // Release table if Dine-In if(o.bord){ const b=bords.find(b=>b.id===o.bord.id); if(b&&status==='Fullført')b.status='ledig'; } // Remove from kitchen after short delay for user feedback toast(status==='Fullført'?'✅ Ordre fullført og sendt til Ordrer':'✕ Ordre kansellert'); setTimeout(()=>{ KITCHEN_ORDERS=KITCHEN_ORDERS.filter(x=>x.id!==id); updKjkBadge(); buildKjokken(); }, 1500); } updKjkBadge(); buildKjokken(); if(status!=='Fullført'&&status!=='Kansellert'){ toast(status==='Venter'?'⏳ Venter — timer pauset':'🔥 Pågår — timer startet'); } } function getTimerMins(type){ const def=parseInt(el('timerDefault')?.value)||15; if(type==='Dine-In')return parseInt(el('timerDineIn')?.value)||def||15; if(type==='Takeaway')return parseInt(el('timerTakeaway')?.value)||def||15; if(type==='Levering')return parseInt(el('timerLevering')?.value)||def||15; return def; } function updKjkBadge(){ const active=KITCHEN_ORDERS.filter(o=>o.status!=='Fullført'&&o.status!=='Kansellert').length; const b=el('kjkBadge'); if(b){b.textContent=active;b.style.display=active>0?'':'none';} } function saveTimerSettings(){ const def=el('timerDefault')?.value||15; const di=el('timerDineIn')?.value||15; const ta=el('timerTakeaway')?.value||10; const le=el('timerLevering')?.value||20; toast('✓ Timer lagret — Standard:'+def+'min · Dine-In:'+di+'min · Takeaway:'+ta+'min · Levering:'+le+'min'); } function buildCatListMeny(){ const cl=el('catListMeny');if(!cl)return; if(!CATS.length){ cl.innerHTML='
Ingen kategorier ennå.
'; return; } cl.innerHTML=CATS.map((cat,i)=>{ const escaped=cat.replace(/'/g,"\'"); return`
${cat} ${MENU.filter(m=>m.cat===cat).length} retter
`; }).join(''); } function moveAlgM(idx,dir){ const ni=idx+dir;if(ni<0||ni>=ALGS.length)return; [ALGS[idx],ALGS[ni]]=[ALGS[ni],ALGS[idx]]; buildAlgListMeny(); } function moveCatM(idx,dir){ const ni=idx+dir;if(ni<0||ni>=CATS.length)return; [CATS[idx],CATS[ni]]=[CATS[ni],CATS[idx]]; buildCatListMeny();buildPOS(); } function deleteCatM(name){ const count=MENU.filter(m=>m.cat===name).length; if(count>0){toast('⚠ Kan ikke slette — '+count+' retter er i kategorien. Flytt rettene først.');return;} CATS=CATS.filter(c=>c!==name); buildCatListMeny();buildPOS(); toast('✓ Kategori slettet: '+name); } function addCategoryMeny(){ const inp=el('newCatInputMeny'); const name=inp.value.trim(); if(!name){toast('⚠ Skriv inn kategorinavn');return;} if(CATS.includes(name)){toast('⚠ Kategorien finnes allerede');return;} CATS.push(name);inp.value=''; buildCatListMeny();buildPOS(); toast('✓ Kategori lagt til: '+name); } function buildAlgListMeny(){ const al=el('algListMeny');if(!al)return; if(!ALGS.length){ al.innerHTML='
Ingen allergener ennå.
'; return; } al.innerHTML=ALGS.map((a,i)=>{ const escapedId=a.id.replace(/'/g,"\'"); return`
${a.icon} ${a.name} ${MENU.filter(m=>m.algs.includes(a.id)).length} retter
`; }).join(''); } function addAllergenMeny(){ const icon=el('newAlgEmojiMeny').value.trim(); const name=el('newAlgNameMeny').value.trim(); if(!icon||!name){toast('⚠ Fyll inn ikon og navn');return;} const id='custom_'+Date.now(); ALGS.push({id,icon,name}); el('newAlgEmojiMeny').value='';el('newAlgNameMeny').value=''; buildAlgListMeny();buildMenyTbl();buildPOS(); toast('✓ Allergen lagt til: '+name); } function deleteAlgM(id){ const inUse=MENU.some(m=>m.algs.includes(id)); if(inUse){toast('⚠ Allergen er i bruk. Fjern det fra retter først.');return;} ALGS=ALGS.filter(a=>a.id!==id); buildAlgListMeny(); toast('✓ Allergen slettet');saveToStorage(); } /* ── RENAME / EDIT ── */ function renameCat(name){ const html=`
✏️ Rediger kategori
`; document.body.insertAdjacentHTML('beforeend',html); setTimeout(()=>{const i=el('editCatInp');if(i){i.focus();i.select();}},50); } function doRenameCat(oldName){ const newName=(el('editCatInp')?.value||'').trim(); el('editModal')?.remove(); if(!newName||newName===oldName)return; if(CATS.includes(newName)){toast('⚠ Kategorinavn er allerede i bruk');return;} MENU.forEach(m=>{if(m.cat===oldName)m.cat=newName;}); CATS=CATS.map(c=>c===oldName?newName:c); buildCatListMeny();buildMenyTbl();buildPOS(); toast('✓ Kategori omdøpt til: '+newName); } function renameAlg(id){ const alg=ALGS.find(a=>a.id===id);if(!alg)return; const html=`
✏️ Rediger allergen
Skriv inn et emoji-tegn som ikon. Eksempel: 🌾 🥛 🥚 🥜 🐟 🦐
`; document.body.insertAdjacentHTML('beforeend',html); setTimeout(()=>{const i=el('editAlgName');if(i){i.focus();i.select();}},50); } function doRenameAlg(id){ const alg=ALGS.find(a=>a.id===id); const icon=(el('editAlgIcon')?.value||'').trim(); const name=(el('editAlgName')?.value||'').trim(); el('editModal')?.remove(); if(!alg)return; if(icon)alg.icon=icon; if(name)alg.name=name; buildAlgListMeny();buildMenyTbl();buildPOS(); toast('✓ Allergen oppdatert: '+alg.name); } /* ── FRIGJØR BORD ── */ function visFrigjorModal(id, num){ document.body.insertAdjacentHTML('beforeend',`
🪑
Bord ${num}
Dette bordet er opptatt. Vil du frigjøre det?
`); } function frigjorAlleBord(){ if(!confirm('Frigjør alle bord til ledig?')) return; bords.forEach(b=>b.status='ledig'); renderMap();renderBL(); saveToStorage(); toast('✅ Alle bord frigjort'); } function frigjorBord(id){ const b=bords.find(x=>x.id===id); if(!b)return; b.status='ledig'; renderMap(); if(typeof renderBL==='function')renderBL(); if(typeof showBPZone==='function'){ const zone=b.zone||'Inne'; showBPZone(zone,null); } saveToStorage(); toast('✅ Bord '+b.num+' frigjort'); } /* ── LIVE KITCHEN TIMER TICKER ── */ setInterval(()=>{ // Update all visible timer displays KITCHEN_ORDERS.forEach(o=>{ if((o.status==='Pågår'||o.status==='Hold')&&o.timerEnd){ const el2=document.getElementById('timer_'+o.id); if(el2){ const secs=Math.max(0,Math.ceil((o.timerEnd-Date.now())/1000)); const m=Math.floor(secs/60),s=secs%60; el2.textContent=m+':'+(s<10?'0':'')+s; el2.style.color=secs<60?'var(--ac2)':secs<120?'var(--wa)':'var(--gr)'; if(secs===0)el2.textContent='Ferdig!'; } } }); // Update badge updKjkBadge(); }, 1000); /* ── MENY TAB SWITCHING ── */ // curMenyTab declared in state block above function switchMenyTab(tab, btn){ curMenyTab = tab; // Reset all tab buttons to inactive style document.querySelectorAll('.meny-tab').forEach(b=>{ b.classList.remove('on'); b.style.borderColor = 'var(--bdr)'; b.style.background = 'var(--surf)'; b.style.color = 'var(--t2)'; }); // Activate clicked button if(btn){ btn.classList.add('on'); btn.style.borderColor = 'var(--ac)'; btn.style.background = 'var(--acl)'; btn.style.color = 'var(--ac)'; } // Hide all tab panels const panels = ['Retter','Kategorier','Allergener']; panels.forEach(t => { const p = el('menyTab'+t); if(p) p.style.display = 'none'; }); // Show selected panel const active = el('menyTab' + tab.charAt(0).toUpperCase() + tab.slice(1)); if(active) active.style.display = 'block'; // Update header title and subtitle const titles = {retter:'Retter', kategorier:'Kategorier', allergener:'Allergener'}; const subs = {retter:'Alle retter i menyen', kategorier:'Administrer menykategorier', allergener:'Administrer allergener'}; const titleEl = el('menyPageTitle'); const subEl = el('menyPageSub'); if(titleEl) titleEl.textContent = titles[tab] || tab; if(subEl) subEl.textContent = subs[tab] || ''; // Update header action buttons per tab const actEl = el('menyPageActions'); if(actEl){ if(tab === 'retter'){ actEl.innerHTML = ` `; } else if(tab === 'kategorier'){ actEl.innerHTML = ` `; } else if(tab === 'allergener'){ actEl.innerHTML = ` `; } actEl.style.display = 'flex'; } // Build content for the selected tab if(tab === 'retter') buildMenyTbl(); if(tab === 'kategorier') buildCatListMeny(); if(tab === 'allergener') buildAlgListMeny(); } /* ── KVITTERING BREDDE ── */ let kvPrintMm=80; function setKvWidth(mm, btn){ kvPrintMm=mm; // 58mm ≈ 219px, 80mm ≈ 302px at 96dpi const px = mm===58 ? 219 : 302; const prev = document.querySelector('.kvit-prev'); if(prev) prev.style.width = px + 'px'; const area = el('kvArea'); if(area) area.style.width = '100%'; document.querySelectorAll('#kvW58,#kvW80').forEach(b=>b.classList.remove('on')); btn.classList.add('on'); } /* ── BORD ZONES ── */ let ZONES=['Inne','Terrasse','Bar']; let activeZone=ZONES[0]; function buildZoneTabs(){ const row=el('zoneTabRow'); if(!row)return; row.innerHTML=ZONES.map(z=>``).join(''); } function switchZone(zone,btn){ activeZone=zone; buildZoneTabs(); renderMap(); } function buildZoneList(){ const zl=el('zoneList');if(!zl)return; zl.innerHTML=ZONES.map((z,i)=>{ const ez=z.replace(/\\/g,'\\\\').replace(/'/g,"\\'"); return`
${z} ${bords.filter(b=>(b.zone||'Inne')===z).length} bord ${i>0?``:'Standard'}
`; }).join(''); // Rebuild editBZone select const sel=el('editBZone'); if(sel)sel.innerHTML=ZONES.map(z=>``).join(''); } function addZone(){ const html=`
➕ Ny sone
`; document.body.insertAdjacentHTML('beforeend',html); setTimeout(()=>el('newZoneInp')?.focus(),50); } function confirmAddZone(){ const name=(el('newZoneInp')?.value||'').trim(); el('addZoneModal')?.remove(); if(!name)return; if(ZONES.includes(name)){toast('⚠ Sonen finnes allerede');return;} ZONES.push(name); buildZoneTabs();buildZoneList(); saveToStorage();toast('✓ Sone opprettet: '+name); } function deleteZone(name){ const cnt=bords.filter(b=>(b.zone||'Inne')===name).length; if(cnt>0){toast('⚠ '+cnt+' bord er i denne sonen. Flytt dem først.');return;} ZONES=ZONES.filter(z=>z!==name); if(activeZone===name)activeZone=ZONES[0]; buildZoneTabs();buildZoneList();renderMap(); saveToStorage();toast('✓ Sone slettet: '+name); } /* ── BORD SHAPE ── */ function setShape(shape, btn){ // Update button styles ['shapeRect','shapeRound','shapeLong'].forEach(id=>{ const b=el(id);if(!b)return; b.style.borderColor='var(--bdr)';b.style.background='var(--surf)';b.style.color='var(--t2)'; }); btn.style.borderColor='var(--ac)';btn.style.background='var(--acl)';btn.style.color='var(--ac)'; // Update selected bord const b=bords.find(x=>x.id===bordSel); if(b){b.shape=shape;renderMap();} } function getShapeBtns(shape){ ['shapeRect','shapeRound','shapeLong'].forEach(id=>{ const b=el(id);if(!b)return; const isActive=(id==='shapeRect'&&shape==='rect')||(id==='shapeRound'&&shape==='round')||(id==='shapeLong'&&shape==='long'); b.style.borderColor=isActive?'var(--ac)':'var(--bdr)'; b.style.background=isActive?'var(--acl)':'var(--surf)'; b.style.color=isActive?'var(--ac)':'var(--t2)'; }); } /* ── BORD COLOR ── */ const BORD_COLORS={ green:{bg:'#D1FAE5',border:'#10B981',text:'#065F46'}, blue:{bg:'#DBEAFE',border:'#3B82F6',text:'#1E3A8A'}, purple:{bg:'#EDE9FE',border:'#7C3AED',text:'#3730A3'}, orange:{bg:'#FEF3C7',border:'#F59E0B',text:'#92400E'}, red:{bg:'#FEE2E2',border:'#EF4444',text:'#7F1D1D'}, pink:{bg:'#FCE7F3',border:'#EC4899',text:'#831843'}, gray:{bg:'#F3F4F6',border:'#9CA3AF',text:'#374151'}, teal:{bg:'#CCFBF1',border:'#14B8A6',text:'#134E4A'}, }; function setBordColor(colorId, btn){ // Highlight selected color document.querySelectorAll('[id^="color_"]').forEach(b=>{ b.style.transform='scale(1)'; b.style.outline='none'; }); btn.style.transform='scale(1.25)'; btn.style.outline='3px solid var(--ac)'; // Apply to selected bord const b=bords.find(x=>x.id===bordSel); if(b){b.colorId=colorId;renderMap();} } function getBordColors(b){ if(b.colorId&&BORD_COLORS[b.colorId])return BORD_COLORS[b.colorId]; // Default based on status if(b.status==='opptatt')return{bg:'#FEF3C7',border:'#F59E0B',text:'#92400E'}; if(b.status==='reservert')return{bg:'#FEE2E2',border:'#EF4444',text:'#7F1D1D'}; return{bg:'#D1FAE5',border:'#10B981',text:'#065F46'}; } function getColorBtns(colorId){ document.querySelectorAll('[id^="color_"]').forEach(b=>{ const id=b.id.replace('color_',''); const active=id===colorId; b.style.transform=active?'scale(1.25)':'scale(1)'; b.style.outline=active?'3px solid var(--ac)':'none'; }); } function renameZone(name){ const html=`
✏️ Gi nytt navn til sone
`; document.body.insertAdjacentHTML('beforeend',html); setTimeout(()=>{const i=el('renZoneInp');if(i){i.focus();i.select();}},50); } function confirmRenameZone(oldName){ const newName=(el('renZoneInp')?.value||'').trim(); el('renZoneModal')?.remove(); if(!newName||newName===oldName)return; if(ZONES.includes(newName)){toast('⚠ Sonenavnet er allerede i bruk');return;} // Update all bords in this zone bords.forEach(b=>{if((b.zone||'Inne')===oldName)b.zone=newName;}); ZONES=ZONES.map(z=>z===oldName?newName:z); if(activeZone===oldName)activeZone=newName; buildZoneTabs();buildZoneList();renderMap();renderBL(); toast('✓ Sone omdøpt til: '+newName); } /* ══════════════════════════════════════════════ BRUKERHÅNDTERING ══════════════════════════════════════════════ */ let BRUKERE=[ {id:1,navn:'Ole Berg',init:'OB',pin:'1234',rolle:'Kasserer',aktiv:true}, {id:2,navn:'Tone Hansen',init:'TH',pin:'5678',rolle:'Kasserer',aktiv:true}, {id:3,navn:'Manager',init:'MG',pin:'9999',rolle:'Admin',aktiv:true}, {id:4,navn:'Super Admin',init:'SA',pin:'0000',rolle:'Super Admin',aktiv:true}, ]; let LOGIN_LOG=[]; function buildBrukere(){ const isSuper = curUser && curUser.superadmin; const isAdmin = curUser && curUser.admin && !curUser.superadmin; // Filter: Admin sees only non-superadmin users const visibleBrukere = isSuper ? BRUKERE : BRUKERE.filter(b => b.rolle !== 'Super Admin'); el('brukerCnt').textContent = visibleBrukere.length; el('brukerBody').innerHTML = visibleBrukere.map(b => { const isSuperAdminRow = b.rolle === 'Super Admin'; // Admin cannot edit/delete Super Admin rows const canEdit = isSuper || !isSuperAdminRow; // Only Super Admin can edit other Super Admin accounts // Admin can edit Kasserer and Admin roles, but NOT change PIN for Admin (only own account) const canChangePIN = isSuper; // only superadmin can change any PIN return `
${b.navn}
${b.init}
•••• ${b.rolle}
${canEdit ? `` : `🔒 Kun Super Admin`} ${canEdit && b.rolle !== 'Super Admin' ? `` : ''}
`; }).join(''); // Login log const ll=el('loginLog'); if(LOGIN_LOG.length){ ll.innerHTML=`${LOGIN_LOG.slice(-10).reverse().map(l=>``).join('')}
TidspunktBrukerRolleStatus
${l.tid}${l.navn}${l.rolle}${l.ok?'OK':'Feil PIN'}
`; } } function toggleBruker(id){ const b=BRUKERE.find(x=>x.id===id);if(!b)return; b.aktiv=!b.aktiv;buildBrukere(); toast(b.aktiv?'✓ '+b.navn+' aktivert':'⏸ '+b.navn+' deaktivert'); } function slettBruker(id){ const b=BRUKERE.find(x=>x.id===id);if(!b)return; if(!confirm('Slett '+b.navn+'?'))return; BRUKERE=BRUKERE.filter(x=>x.id!==id); // Also remove from PINS delete PINS[b.pin]; buildBrukere();toast('✓ Bruker slettet'); } function redigerBruker(id, canChangePIN=false){ const b=BRUKERE.find(x=>x.id===id);if(!b)return; const html=`
✏️ Rediger bruker
${!canChangePIN?'
🔒 Kun Super Admin kan endre PIN-koder
':''}
`; document.body.insertAdjacentHTML('beforeend',html); setTimeout(()=>el('bNavn')?.focus(),50); } function lagreBruker(id){ const b=BRUKERE.find(x=>x.id===id);if(!b)return; const navn=el('bNavn').value.trim(); const init=el('bInit').value.trim().toUpperCase().slice(0,2); const pinInp=el('bPin'); const pin=pinInp&&!pinInp.disabled?pinInp.value.trim():''; const rolle=el('bRolle').value; el('brukerModal').remove(); if(pin&&pin.length===4){ delete PINS[b.pin]; b.pin=pin; PINS[pin]={n:navn,i:init,r:rolle,admin:rolle==='Admin'||rolle==='Super Admin',superadmin:rolle==='Super Admin'}; } else { // Keep existing PIN, just update name/role in PINS PINS[b.pin]={n:navn,i:init,r:rolle,admin:rolle==='Admin'||rolle==='Super Admin',superadmin:rolle==='Super Admin'}; } b.navn=navn;b.init=init;b.rolle=rolle; buildBrukere();toast('✓ Bruker oppdatert: '+navn); } function openNyBruker(){ const html=`
+ Ny bruker
`; document.body.insertAdjacentHTML('beforeend',html); setTimeout(()=>el('bNavn')?.focus(),50); } function lagreNyBruker(){ const navn=el('bNavn').value.trim(); const init=el('bInit').value.trim().toUpperCase().slice(0,2)||navn.split(' ').map(w=>w[0]).join('').slice(0,2).toUpperCase(); const pin=el('bPin').value.trim(); const rolle=el('bRolle').value; el('brukerModal').remove(); if(!navn||pin.length!==4){toast('⚠ Fyll inn navn og 4-sifret PIN');return;} if(PINS[pin]){toast('⚠ PIN er allerede i bruk');return;} const id=Date.now(); BRUKERE.push({id,navn,init,pin,rolle,aktiv:true}); PINS[pin]={n:navn,i:init,r:rolle,admin:rolle==='Admin',superadmin:false}; buildBrukere();saveToStorage();toast('✓ Bruker opprettet: '+navn); } /* ══════════════════════════════════════════════ KASSAOPPGJØR ══════════════════════════════════════════════ */ function buildOppgjor(){ const now=new Date(); const datStr=now.toLocaleDateString('nb-NO',{weekday:'long',year:'numeric',month:'long',day:'numeric'}); el('oppgjorDato').textContent=datStr+' · '+now.toLocaleTimeString('nb-NO',{hour:'2-digit',minute:'2-digit'}); // Use actual bilag archive data + mock for demo const bilag=bilagArchiveArr; const totalt=bilag.reduce((s,b)=>s+b.total,0); const antall=bilag.length; const snitt=antall>0?totalt/antall:0; // KPI cards const kpis=[ {lbl:'Totalt salg',val:nok(totalt),sub:antall+' ordrer',color:'var(--ac)'}, {lbl:'Kontant',val:nok(totalt*0.18),sub:'Estimert',color:'var(--gr)'}, {lbl:'Kort / Vipps',val:nok(totalt*0.82),sub:'Estimert',color:'var(--ac)'}, {lbl:'MVA skyldig',val:nok(totalt*0.142),sub:'15% + 25%',color:'var(--ac2)'}, ]; el('oppgjorKPI').innerHTML=kpis.map(k=>`
${k.lbl}
${k.val}
${k.sub}
`).join(''); // Betalingsmetoder const betal=[ {m:'💳 Kort',v:totalt*0.54}, {m:'📱 Vipps',v:totalt*0.28}, {m:'💵 Kontant',v:totalt*0.18}, ]; el('oppgjorBetal').innerHTML=betal.map(b=>`
${b.m} ${nok(b.v)}
`).join(''); // MVA const mva=[ {sats:'25% (drikke/alkohol)',grunnlag:totalt*0.18,mva:totalt*0.18*0.2}, {sats:'15% (mat, dine-in)',grunnlag:totalt*0.52,mva:totalt*0.52*0.1304}, {sats:'12% (mat, take/lev)',grunnlag:totalt*0.30,mva:totalt*0.30*0.1071}, ]; el('oppgjorMva').innerHTML=mva.map(m=>`
MVA ${m.sats}
Grunnlag: ${nok(m.grunnlag)}
${nok(m.mva)}
`).join('')+ `
Total MVA ${nok(mva.reduce((s,m)=>s+m.mva,0))}
`; // Bilagslogg el('oppgjorBilag').innerHTML=bilag.length? `${bilag.slice().reverse().map(b=>``).join('')}
BilagTidspunktTypeBeløp
${b.num}${b.time.toLocaleString('nb-NO')}${b.type}${nok(b.total)}
`: '
Ingen bilag registrert ennå. Gjør et salg i POS.
'; } function printXRapport(){ const w=window.open('','_blank','width=500,height=700'); if(!w){toast('⚠ Tillat popups');return;} const now=new Date(); const bilag=bilagArchiveArr; const totalt=bilag.reduce((s,b)=>s+b.total,0); w.document.write(`
X-RAPPORT
Fisketorget Bergen · ${now.toLocaleString('nb-NO')}

Antall bilag:${bilag.length}
Totalt salg:${nok(totalt)}

Kortbetaling:${nok(totalt*0.82)}
Kontant:${nok(totalt*0.18)}

MVA 25%:${nok(totalt*0.036)}
MVA 15%:${nok(totalt*0.068)}
MVA 12%:${nok(totalt*0.032)}

Totalt inkl. MVA${nok(totalt)}
*** X-rapport — kassen er ikke nullstilt ***