Campackt

Alles dabei. Einfach los.

Campackt

Alles dabei. Einfach los.
REISE
Urlaub
Deine Packliste ist bereit.
0einzupacken
0gepackt
bereits an Bord
Stelle zuerst deine Packliste zusammen. Danach kannst du direkt scannen oder abhaken.
Aktuelle Reise
Reise verwalten
0 von 0
Noch geht’s los
0 fehlen
0 %

Was noch mit muss

`); w.document.close(); }; // -------- Product scanner -------- let productScanner=null,productScannerPaused=false,currentScannedProductCode=''; async function openProductScanner(){ $('#productScannerDialog').showModal(); $('#productUnknown').classList.add('hidden'); $('#productKnown').classList.add('hidden'); $('#resumeProductScanner').classList.add('hidden'); $('#scannerStatus').textContent='Kamera wird gestartet …'; await startProductCamera(); } async function startProductCamera(){ if(typeof Html5Qrcode==='undefined'){alert('Scanner-Bibliothek konnte nicht geladen werden.');return} if(productScanner)return; productScanner=new Html5Qrcode('productReader'); try{ await productScanner.start( {facingMode:'environment'}, {fps:10,qrbox:{width:280,height:160},formatsToSupport:[ Html5QrcodeSupportedFormats.QR_CODE,Html5QrcodeSupportedFormats.EAN_13, Html5QrcodeSupportedFormats.EAN_8,Html5QrcodeSupportedFormats.CODE_128, Html5QrcodeSupportedFormats.UPC_A,Html5QrcodeSupportedFormats.UPC_E ]}, code=>{if(!productScannerPaused) productCodeDetected(code)} ); $('#scannerStatus').textContent='Bereit zum Scannen'; }catch(e){ $('#scannerStatus').textContent='Kamera konnte nicht gestartet werden.'; alert('Kamera konnte nicht gestartet werden. Bitte Kamera-Berechtigung für Safari prüfen.'); } } async function productCodeDetected(code){ productScannerPaused=true; currentScannedProductCode=String(code).trim(); try{await productScanner.pause(true)}catch(e){} $('#productUnknown').classList.add('hidden'); $('#productKnown').classList.add('hidden'); const known=findProductByCode(currentScannedProductCode); if(known){ $('#productKnown').innerHTML=`
Schon vorhanden: ${esc(known.name)}
${esc(currentScannedProductCode)}
`; $('#productKnown').classList.remove('hidden'); $('#resumeProductScanner').classList.remove('hidden'); return; } $('#scannedProductCode').textContent=currentScannedProductCode; $('#scannedProductName').value='';fillCategorySelect('scannedProductCategory','scannedProductCategoryCustom'); $('#scannedProductCategory').value=''; $('#scannedProductLocation').value=''; $('#existingProductSelect').innerHTML=''+[...state.items].sort((a,b)=>a.name.localeCompare(b.name,'de')).map(i=>``).join(''); $('#productUnknown').classList.remove('hidden'); } function campacktResetHorizontalViewport(){ try{document.documentElement.scrollLeft=0}catch(e){} try{document.body.scrollLeft=0}catch(e){} try{window.scrollTo({left:0,top:window.scrollY,behavior:'instant'})}catch(e){try{window.scrollTo(0,window.scrollY)}catch(_){} } } async function resumeProductScannerFn(){ $('#productUnknown').classList.add('hidden'); $('#productKnown').classList.add('hidden'); $('#resumeProductScanner').classList.add('hidden'); currentScannedProductCode=''; productScannerPaused=false; try{await productScanner.resume()}catch(e){ await stopProductCamera(); await startProductCamera(); } requestAnimationFrame(()=>{campacktResetHorizontalViewport();requestAnimationFrame(campacktResetHorizontalViewport)}); } async function stopProductCamera(){ try{if(productScanner)await productScanner.stop()}catch(e){} try{if(productScanner)productScanner.clear()}catch(e){} productScanner=null; productScannerPaused=false; $('#productReader').innerHTML=''; } $('#bulkScanBtn').onclick=openProductScanner; $('#resumeProductScanner').onclick=resumeProductScannerFn; $('#closeProductScanner').onclick=async()=>{await stopProductCamera();$('#productScannerDialog').close();await loadAll();requestAnimationFrame(()=>{campacktResetHorizontalViewport();requestAnimationFrame(campacktResetHorizontalViewport)})}; $('#saveScannedProduct').onclick=async()=>{ const btn=$('#saveScannedProduct'); const name=$('#scannedProductName').value.trim(); const code=String(currentScannedProductCode||'').trim(); if(!name||!code){alert('Bitte Produktname eingeben.');return} if(findProductByCode(code)){alert('Dieser Barcode ist bereits einem Produkt zugeordnet.');return} const oldText=btn.textContent; btn.disabled=true; btn.textContent='Speichere …'; try{ // Neue Produkte werden zuerst ohne Legacy-Barcode angelegt. // So funktioniert die Anlage auch mit der Mehrfach-Barcode-Struktur zuverlässig. const payload={name,barcode:null,category:chosenCategory('scannedProductCategory','scannedProductCategoryCustom')||null,storage_location:$('#scannedProductLocation').value.trim()||null,required:true,note:null}; const {data:item,error}=await sb.from('items').insert(payload).select().single(); if(error)throw error; if(state.itemBarcodeReady){ const {error:barcodeError}=await sb.from('item_barcodes').insert({item_id:item.id,barcode:code,label:null}); if(barcodeError){ // Kein halbfertiges Produkt im Pool hinterlassen. await sb.from('items').delete().eq('id',item.id); throw barcodeError; } }else{ // Fallback fuer alte Datenbanken ohne item_barcodes-Tabelle. const {error:legacyError}=await sb.from('items').update({barcode:code}).eq('id',item.id); if(legacyError){await sb.from('items').delete().eq('id',item.id);throw legacyError} } await loadAll(); await resumeProductScannerFn(); }catch(err){ console.error('Produkt speichern fehlgeschlagen',err); alert('Produkt konnte nicht gespeichert werden: '+(err?.message||String(err))); }finally{btn.disabled=false;btn.textContent=oldText} }; $('#attachScannedCode').onclick=async()=>{ const itemId=$('#existingProductSelect').value; if(!itemId||!currentScannedProductCode){alert('Bitte ein vorhandenes Produkt auswählen.');return} if(!state.itemBarcodeReady){alert('Die Supabase-Erweiterung für mehrere Barcodes fehlt noch.');return} const {error}=await sb.from('item_barcodes').insert({item_id:itemId,barcode:currentScannedProductCode,label:null}); if(error){alert(error.message);return} await loadItemBarcodesV14(); await resumeProductScannerFn(); }; async function toggleProductProfile(itemId,tripId,profileName){ const existing=state.tripItems.find(x=>x.trip_id===tripId&&x.item_id===itemId); if(existing){ const ok=confirm(`"${profileName}" für dieses Produkt entfernen?`); if(!ok)return; const {error}=await sb.from('trip_items').delete().eq('id',existing.id); if(error){alert(error.message);return} }else{ const qty=prompt(`Soll-Menge für "${profileName}"?`,'1'); if(qty===null)return; const {error}=await sb.from('trip_items').insert({ trip_id:tripId, item_id:itemId, target_quantity:Math.max(1,parseInt(qty,10)||1) }); if(error){alert(error.message);return} } await loadAll(); } let editingProductId=null; function renderEditProductCodes(){ const box=$('#editProductCodes'); if(!box||!editingProductId)return; const codes=allCodesForItem(editingProductId); box.innerHTML=codes.length?codes.map(c=>`
${esc(c.barcode)}
${esc(c.label||'Barcode')}
${c.id==='legacy'?'Hauptcode':``}
`).join(''):'
Noch kein Barcode hinterlegt.
'; } function openEditProduct(itemId){ const i=state.items.find(x=>x.id===itemId); if(!i)return; editingProductId=itemId; $('#editProductId').value=itemId; $('#editProductName').value=i.name||''; fillCategorySelect('editProductCategory','editProductCategoryCustom',i.category||''); bindCategorySelect('editProductCategory','editProductCategoryCustom'); $('#editProductLocation').value=i.storage_location||''; $('#editProductNote').value=i.note||''; $('#editProductRequired').value=i.required===false?'false':'true'; $('#editNewBarcode').value=''; renderEditProductCodes(); $('#editProductDialog').showModal(); } $('#cancelEditProduct').onclick=()=>$('#editProductDialog').close(); $('#saveEditProduct').onclick=async()=>{ if(!editingProductId)return; const name=$('#editProductName').value.trim(); if(!name)return alert('Bitte einen Produktnamen eingeben.'); const payload={name,category:chosenCategory('editProductCategory','editProductCategoryCustom')||null,storage_location:$('#editProductLocation').value.trim()||null,note:$('#editProductNote').value.trim()||null,required:$('#editProductRequired').value==='true'}; const {error}=await sb.from('items').update(payload).eq('id',editingProductId); if(error)return alert(error.message); $('#editProductDialog').close(); await loadAll(); }; $('#addEditBarcode').onclick=async()=>{ if(!editingProductId)return; const code=$('#editNewBarcode').value.trim(); if(!code)return; if(findProductByCode(code))return alert('Dieser Code ist bereits einem Produkt zugeordnet.'); if(!state.itemBarcodeReady)return alert('Die Supabase-Erweiterung für mehrere Barcodes fehlt noch.'); const {error}=await sb.from('item_barcodes').insert({item_id:editingProductId,barcode:code,label:null}); if(error)return alert(error.message); $('#editNewBarcode').value=''; await loadItemBarcodesV14(); renderEditProductCodes(); }; async function removeEditBarcode(barcodeId){ if(!confirm('Diesen Barcode wirklich vom Produkt entfernen?'))return; const {error}=await sb.from('item_barcodes').delete().eq('id',barcodeId); if(error)return alert(error.message); await loadItemBarcodesV14(); renderEditProductCodes(); } // ---------- Robust product actions ---------- async function deleteProductPermanent(itemId,name){ const ok=confirm(`"${name}" wirklich dauerhaft löschen?\n\nDas Produkt wird aus dem Produkt-Pool, allen Profilen und allen Packlisten entfernt.`); if(!ok)return; // Delete dependent records explicitly first. This also works if an older DB relation // was created without ON DELETE CASCADE. const steps=[ sb.from('scans').delete().eq('item_id',itemId), sb.from('trip_items').delete().eq('item_id',itemId) ]; if(state.itemBarcodeReady) steps.push(sb.from('item_barcodes').delete().eq('item_id',itemId)); const results=await Promise.all(steps); const depError=results.find(r=>r.error)?.error; if(depError){alert('Löschen nicht möglich: '+depError.message);return} const {error}=await sb.from('items').delete().eq('id',itemId); if(error){alert('Löschen nicht möglich: '+error.message);return} alert(`"${name}" wurde dauerhaft gelöscht.`); await loadAll(); } document.addEventListener('click',async e=>{ const btn=e.target.closest('[data-action]'); if(!btn)return; const action=btn.dataset.action; if(action==='assign-profile'){ await toggleProductProfile( btn.dataset.itemId, btn.dataset.tripId, btn.dataset.profileName||'Profil' ); return; } const id=btn.dataset.id; if(action==='edit-product'){ openEditProduct(id); }else if(action==='remove-barcode'){ await removeEditBarcode(btn.dataset.barcodeId); }else if(action==='delete-product'){ await deleteProductPermanent(id,btn.dataset.name||'Produkt'); }else if(action==='show-qr'){ const item=state.items.find(i=>i.id===id); const own=allCodesForItem(id).find(c=>String(c.barcode).startsWith('ROSA-')); if(!item||!own){alert('Für dieses Produkt wurde noch kein eigener Rosa-QR gefunden.');return} showProductQrModal(id,item.name,own.barcode); }else if(action==='show-label'){ window.showProductLabel(id); } }); $('#newQrProductBtn').onclick=()=>{ $('#newQrProductName').value=''; fillCategorySelect('newQrProductCategory','newQrProductCategoryCustom'); $('#newQrProductLocation').value=''; $('#newQrProductDialog').showModal(); }; $('#saveNewQrProduct').onclick=async e=>{ e.preventDefault(); const name=$('#newQrProductName').value.trim(); if(!name){alert('Bitte einen Produktnamen eingeben.');return} const code=rosaOwnCode(); const payload={ name, barcode:null, category:chosenCategory('newQrProductCategory','newQrProductCategoryCustom')||null, storage_location:$('#newQrProductLocation').value.trim()||null, required:true, note:null }; const {data:item,error}=await sb.from('items').insert(payload).select().single(); if(error){alert(error.message);return} let qrStored=false; if(state.itemBarcodeReady){ const r=await sb.from('item_barcodes').insert({item_id:item.id,barcode:code,label:'Rosa eigener QR'}); if(r.error){alert('Produkt wurde angelegt, aber QR konnte nicht gespeichert werden: '+r.error.message);return} qrStored=true; }else{ const r=await sb.from('items').update({barcode:code}).eq('id',item.id); if(r.error){alert(r.error.message);return} item.barcode=code; qrStored=true; } if(qrStored){ $('#newQrProductDialog').close(); await loadAll(); showProductQrModal(item.id,item.name,code); } }; // ===== v20 UX layer ===== let v20LastScan=null; let v20Skipped=new Set(); function v20Toast(msg){ const el=document.getElementById('v20Toast'); if(!el)return; el.textContent=msg; el.classList.add('show'); setTimeout(()=>el.classList.remove('show'),1500); } function v20Haptic(){ let vibrated=false; try{ if(navigator.vibrate) vibrated=!!navigator.vibrate(70); }catch(e){} // iPhone Safari currently has no Vibration API. Give an immediate tactile-like fallback via sound + visual pulse. try{ const Ctx=window.AudioContext||window.webkitAudioContext; if(Ctx){const ctx=new Ctx(),osc=ctx.createOscillator(),gain=ctx.createGain();osc.type='sine';osc.frequency.value=180;gain.gain.setValueAtTime(0.035,ctx.currentTime);gain.gain.exponentialRampToValueAtTime(0.0001,ctx.currentTime+0.045);osc.connect(gain);gain.connect(ctx.destination);osc.start();osc.stop(ctx.currentTime+0.05);} }catch(e){} document.body.classList.remove('scan-feedback'); void document.body.offsetWidth; document.body.classList.add('scan-feedback'); setTimeout(()=>document.body.classList.remove('scan-feedback'),160); return vibrated; } function v20Profiles(){ try{return typeof uniqueProfiles==='function'?uniqueProfiles():state.trips}catch(e){return state.trips||[]} } function v20SyncListSelect(){ const sel=document.getElementById('v20ListSelect'); if(!sel)return; const profiles=v20Profiles(); sel.innerHTML=profiles.map(t=>``).join(''); sel.value=state.listName; requestAnimationFrame(()=>{sel.value=state.listName;}); } function v20ResetCurrent(){ const tid=currentTripId(); if(!tid)return; const label=state.mode==='out'?'Hinfahrt':'Rückfahrt'; if(!confirm(`${label} für "${state.listName}" wirklich neu starten?\n\nDie Liste selbst bleibt erhalten.`))return; (async()=>{ const r1=await sb.from('scans').delete().eq('trip_id',tid).eq('direction',state.mode); if(r1.error){alert(r1.error.message);return} try{await sb.from('ingredient_scans').delete().eq('profile_id',tid).eq('direction',state.mode)}catch(e){} v20Skipped=new Set(); v20Toast('Packstatus zurückgesetzt'); await loadAll(); })(); } async function v20UndoLast(){ const tid=currentTripId(); const normal=(state.scans||[]).filter(s=>s.trip_id===tid&&s.direction===state.mode).sort((a,b)=>new Date(b.created_at)-new Date(a.created_at)); const ingredient=(state.ingredientScans||[]).filter(s=>s.profile_id===tid&&s.direction===state.mode).sort((a,b)=>new Date(b.created_at)-new Date(a.created_at)); const a=normal[0],b=ingredient[0]; if(!a&&!b){v20Toast('Noch nichts zum Rückgängigmachen');return} const useIngredient=!a||(b&&new Date(b.created_at)>new Date(a.created_at)); const table=useIngredient?'ingredient_scans':'scans'; const id=(useIngredient?b:a).id; const {error}=await sb.from(table).delete().eq('id',id); if(error){alert(error.message);return} v20Toast('Letzten Scan rückgängig'); await loadAll(); } function v20Wire(){ const sel=document.getElementById('v20ListSelect'); if(sel){ v20SyncListSelect(); sel.onchange=()=>{ state.listName=sel.value; localStorage.setItem('rosa-list',state.listName); renderAll(); v20SyncListSelect(); }; } const menuBtn=document.getElementById('v20MenuButton'); const menu=document.getElementById('v20ManageMenu'); if(menuBtn&&menu)menuBtn.onclick=()=>{menu.open=!menu.open}; const reset=document.getElementById('v20Reset'); if(reset)reset.onclick=v20ResetCurrent; const undo=document.getElementById('v20Undo'); if(undo)undo.onclick=v20UndoLast; const products=document.getElementById('v20EditProducts'); if(products)products.onclick=()=>showView('items'); const newList=document.getElementById('v20NewList'); if(newList)newList.onclick=()=>{ const name=prompt('Name der neuen Liste:'); if(!name)return; (async()=>{ const {data,error}=await sb.from('trips').insert({name:name.trim(),active:true}).select().single(); if(error){alert(error.message);return} state.listName=data.name;localStorage.setItem('rosa-list',data.name); await loadAll();v20SyncListSelect(); })(); }; const dup=document.getElementById('v20Duplicate'); if(dup)dup.onclick=()=>{ const name=prompt(`"${state.listName}" duplizieren als:`,state.listName+' Kopie'); if(!name)return; (async()=>{ const source=currentTripId(); const {data:newTrip,error}=await sb.from('trips').insert({name:name.trim(),active:true}).select().single(); if(error){alert(error.message);return} const rows=(state.tripItems||[]).filter(x=>x.trip_id===source).map(x=>({trip_id:newTrip.id,item_id:x.item_id,target_quantity:x.target_quantity})); if(rows.length){ const r=await sb.from('trip_items').insert(rows); if(r.error){alert(r.error.message);return} } state.listName=newTrip.name;localStorage.setItem('rosa-list',newTrip.name); await loadAll();v20SyncListSelect(); })(); }; } // Wrap renderAll so v20 controls stay synced after realtime updates. if(typeof renderAll==='function'){ const _v20RenderAll=renderAll; renderAll=function(){ _v20RenderAll(); v20Wire(); v20SyncListSelect(); const t=typeof totals==='function'?totals():{total:0,done:0,missing:0}; const head=document.getElementById('v20ListHeading'); if(head)head.textContent=t.missing===0&&t.total>0?'Alles eingepackt':'Was noch mit muss'; const list=document.getElementById('list'); if(list && t.missing===0 && t.total>0 && document.getElementById('filter')?.value==='missing'){ list.innerHTML='
Alles drin.Alles gepackt. Du kannst los.
'; } } } // Successful scan feedback by wrapping plusOne / ingredient scan if available. if(typeof window.plusOne==='function'){ const _plusOne=window.plusOne; window.plusOne=async function(itemId){ const r=await _plusOne(itemId); const item=(state.items||[]).find(i=>i.id===itemId); v20Haptic();v20Toast(item?`${item.name} eingepackt`:'Eingepackt'); v20LastScan={type:'item',id:itemId}; return r; } } if(typeof window.plusIngredient==='function'){ const _plusIngredient=window.plusIngredient; window.plusIngredient=async function(id){ const r=await _plusIngredient(id); const ing=(state.ingredients||[]).find(i=>i.id===id); v20Haptic();v20Toast(ing?`${ing.name} eingepackt`:'Zutat eingepackt'); v20LastScan={type:'ingredient',id}; return r; } } setTimeout(v20Wire,0); // ===== v21.1 compact management sheet ===== function v211Sync(){ const title=document.getElementById('listTitle'); if(title) title.textContent=state.listName||'Packliste'; const ps=document.getElementById('v211Packer'); if(ps){ const old=document.getElementById('packerSelect'); if(old && old.options.length){ ps.innerHTML=old.innerHTML; ps.value=old.value; }else{ ps.innerHTML=(state.packers||[]).map(p=>``).join(''); } } } async function v211RenameCurrent(){ const trip=(v20Profiles()||[]).find(t=>t.name===state.listName); if(!trip)return alert('Aktuelle Packliste nicht gefunden.'); const next=prompt('Neuer Name der Packliste:',state.listName); if(!next||!next.trim()||next.trim()===state.listName)return; const name=next.trim(); if((v20Profiles()||[]).some(t=>t.id!==trip.id&&String(t.name).toLowerCase()===name.toLowerCase()))return alert('Eine Packliste mit diesem Namen gibt es bereits.'); const {error}=await sb.from('trips').update({name}).eq('id',trip.id); if(error)return alert(error.message); state.listName=name;localStorage.setItem('rosa-list',name); v211Close();await loadAll();v20SyncListSelect();v20Toast('Packliste umbenannt'); } async function v211DeleteCurrent(){ const profiles=v20Profiles()||[]; const trip=profiles.find(t=>t.name===state.listName); if(!trip)return alert('Aktuelle Packliste nicht gefunden.'); if(!confirm(`"${state.listName}" wirklich löschen?\n\nDie Packliste und ihr Packstatus werden entfernt. Produkte im Produkt-Pool bleiben erhalten.`))return; try{await sb.from('scans').delete().eq('trip_id',trip.id)}catch(e){} try{await sb.from('ingredient_scans').delete().eq('profile_id',trip.id)}catch(e){} try{await sb.from('trip_items').delete().eq('trip_id',trip.id)}catch(e){} const {error}=await sb.from('trips').delete().eq('id',trip.id); if(error)return alert(error.message); const remaining=profiles.filter(t=>t.id!==trip.id); state.listName=(remaining[0]&&remaining[0].name)||'';if(state.listName)localStorage.setItem('rosa-list',state.listName);else localStorage.removeItem('rosa-list'); v211Close();await loadAll();v20SyncListSelect();v20Toast('Packliste gelöscht'); } function v211Open(){ v211Sync(); document.getElementById('v211Menu')?.classList.add('open'); } function v211Close(){ document.getElementById('v211Menu')?.classList.remove('open'); } setTimeout(()=>{ const menuBtn=document.getElementById('v20MenuButton'); if(menuBtn) menuBtn.onclick=v211Open; document.getElementById('v211Close')?.addEventListener('click',v211Close); document.getElementById('v211Menu')?.addEventListener('click',e=>{ if(e.target.id==='v211Menu')v211Close(); }); document.getElementById('v211New')?.addEventListener('click',()=>{v211Close();document.getElementById('v20NewList')?.click()}); document.getElementById('v211Dup')?.addEventListener('click',()=>{v211Close();document.getElementById('v20Duplicate')?.click()}); document.getElementById('v211Rename')?.addEventListener('click',v211RenameCurrent); document.getElementById('v211Delete')?.addEventListener('click',v211DeleteCurrent); document.getElementById('v211Reset')?.addEventListener('click',()=>{v211Close();document.getElementById('v20Reset')?.click()}); document.getElementById('v211Products')?.addEventListener('click',()=>{v211Close();showView('items')}); document.getElementById('v211Packer')?.addEventListener('change',e=>{ const old=document.getElementById('packerSelect'); if(old){old.value=e.target.value;old.dispatchEvent(new Event('change',{bubbles:true}))} }); v211Sync(); },50); // keep title/sheet in sync after renders if(typeof renderAll==='function'){ const _v211Render=renderAll; renderAll=function(){_v211Render();setTimeout(v211Sync,0)} } window.addEventListener('pageshow',()=>{setTimeout(()=>loadAll(),50)}); document.addEventListener('visibilitychange',()=>{if(!document.hidden)setTimeout(()=>loadAll(),50)}); if('serviceWorker' in navigator)navigator.serviceWorker.register('sw.js'); boot();

Packliste verwalten

Neue Reise

Ein Name reicht. Danach stellst du direkt zusammen, was mit soll.

Campackt sortiert vor

Aus Produktname, Kategorie und deinem Camping-Setup erstellt Campackt eine sinnvolle Startsortierung. Du kannst Ausnahmen danach jederzeit unter „Meine Sachen“ ändern.