LUT-capture
Gespeichert
LUT-capture
Vor-Ort-Erfassung · Heizung & Hülle
Letzte Projekte
⚙ Einstellungen
Projekt
Gebäude
Nein
Ja
Gebäudehülle
Fassaden
Dach
Kellerdecken
Fenstertypen
Hauseingangstüren
Sanierungsfahrplan
Maßnahmen den Paketen zuordnen (Mehrfachauswahl pro Paket)
Dach / OGDFassadeKellerdeckeFenster vor 95Fenster nach 95HeizungWarmwasserLüftungPV
Heizung
Revisionsunterlagen
+ Foto Ordner / Hydraulikschema
Warmwasserbereitung
Verbrauchsdaten
Wärmeübergabe
Geschätzter Anteil an Wohneinheiten
Wärmeerzeuger
Speicher
MAG / Druckhaltung
Abgasführung
+ Foto Abgasführung
Kondensatablauf
Ja
Nein
Unklar
+ Foto Kondensat / Bodenablauf
Heizungsraum
+ Foto Heizungsraum
Deckenhöhen
Max. Speicherhöhen identifizieren
Engstellen / Türen Zuwegung
Brennstofflager
Verteilung & HAB
Rohrsystem
1-Rohr
2-Rohr
Unklar
Nein
Teilw.
Ja
Keine
Ohne VE
Mit VE
Ja
Teilw.
Nein
Ja
Nein
Unklar
+ Foto Verteiler / Stränge
Pumpen
Warmwasser-Verteilung
Ja
Nein
Unklar
Ja
Nein
Unklar
Stichprobe Wohnungen
WP / Elektro / FW
Aufstellort WP
Reines WG
Allg. WG
Mischgebiet
+ Foto Aufstellort
Zuwegung / Transport
Ja
Nein
Unklar
Elektroanschluss
Nein
Ja
+ Foto Zählerschrank (offen + geschlossen)
Fernwärme
Ja
Nein
Geplant
Pläne & Fotos
Pläne (PDF)
+ Plan hinzufügen
Allgemeine Fotos
+ Foto aufnehmen
Fotos & Pläne exportieren
Einzelne Fotos/Pläne separat speichern oder teilen
Allgemeine Notizen
⚙ Einstellungen
0 Fotos · 0 Felder gespeichert
Bericht
1/1
Heizung
'+reportHtmlCache+''); doc.close(); iframe.onload=()=>{ setTimeout(()=>{ iframe.contentWindow.print(); setTimeout(()=>document.body.removeChild(iframe),1000); },300); }; // Fallback if onload doesn't fire setTimeout(()=>{ try{iframe.contentWindow.print();}catch(e){} setTimeout(()=>{try{document.body.removeChild(iframe);}catch(e){}},1000); },1000); } // Single photo/plan export function exportSingleFile(dataUrl,filename){ const arr=dataUrl.split(','); const mime=arr[0].match(/:(.*?);/)[1]; const raw=atob(arr[1]); const u8=new Uint8Array(raw.length); for(let i=0;i0?originalName.substring(0,dot):originalName; const ext=dot>0?originalName.substring(dot):'.pdf'; return yy+mm+dd+'_'+base+'_capt'+ext; } async function exportPlan(){ const plans=[]; for(let i=0;i({label:p.name,cls:'modal-btn-cancel'})); btns.push({label:'Abbrechen',cls:'modal-btn-cancel'}); const r=await showModal('Plan exportieren','Welchen Plan speichern (inkl. Zeichnungen)?',btns); if(r<0||r>=plans.length)return; const planIdx=plans[r].idx; const planPhotos=await getPhotos('plan-'+planIdx); if(!planPhotos.length)return; const dataUrl=planPhotos[0].data; // Load annotations let annots={}; try{ const annoPhotos=await getPhotos('anno-'+planIdx); if(annoPhotos.length>0) annots=JSON.parse(annoPhotos[0].data)||{}; }catch(e){} const origName=plans[r].name||'Plan'; if(dataUrl.startsWith('data:application/pdf')&&typeof PDFLib!=='undefined'){ // Vector PDF export with pdf-lib const raw=atob(dataUrl.split(',')[1]); const arr=new Uint8Array(raw.length); for(let i=0;i{ const pageAnnots=annots[String(idx+1)]; if(!pageAnnots)return; const W=page.getWidth(), H=page.getHeight(); pageAnnots.forEach(a=>{ if(a.type==='pen'){ const pts=a.np||a.points; if(pts.length<2)return; const lw=a.nw?(a.nw*W):(a.width||2); const color=hexToRgb(a.color||'#E53935'); for(let i=1;i{ const cv=document.createElement('canvas'); cv.width=img.width;cv.height=img.height; const ctx=cv.getContext('2d'); ctx.drawImage(img,0,0); // Draw annotations const pageAnnots=annots['1']; if(pageAnnots) pageAnnots.forEach(a=>{ if(a.type==='pen'){ const lw=a.nw?(a.nw*cv.width):(a.width||2); ctx.strokeStyle=a.color;ctx.lineWidth=lw;ctx.lineCap='round';ctx.lineJoin='round'; ctx.beginPath(); const pts=a.np||a.points; pts.forEach((p,i)=>{ const px=a.np?p[0]*cv.width:p[0]; const py=a.np?p[1]*cv.height:p[1]; if(i===0)ctx.moveTo(px,py);else ctx.lineTo(px,py); }); ctx.stroke(); } else if(a.type==='text'){ const px=a.nx!==undefined?a.nx*cv.width:(a.x||0); const py=a.ny!==undefined?a.ny*cv.height:(a.y||0); const fs=a.nfs?(a.nfs*cv.width):18; ctx.font='bold '+Math.round(fs)+'px sans-serif'; ctx.fillStyle=a.color; ctx.fillText(a.text,px,py); } }); cv.toBlob(blob=>{ if(blob) offerDownload(planExportName(origName.replace(/\.[^.]+$/,'')+'.jpg'), blob); },'image/jpeg',0.92); }; img.src=dataUrl; } else { // PDF but no pdf-lib available await showModal('PDF-Export','pdf-lib konnte nicht geladen werden. Bitte Internetverbindung prüfen.',[{label:'OK',cls:'modal-btn-primary'}]); } } // openPlanPrintView kept for PDF report function openPlanPrintView(filename, imageDataUrls){ const imgs=imageDataUrls.map(src=>``).join(''); const html=`
${imgs}
`; showReport(filename, html); } async function exportAllPhotos(){ const all=await getAllPhotos(); const photos=all.filter(p=>!p.cat.startsWith('anno-')); if(photos.length===0){await showModal('Keine Fotos','Es sind keine Fotos vorhanden.',[{label:'OK',cls:'modal-btn-primary'}]);return;} const addr=load('projekt.adresse')||'Projekt'; const prefix=addr.replace(/[^a-zA-Z0-9äöüÄÖÜß\-_]/g,'_').substring(0,20); const d=new Date(); const yy=String(d.getFullYear()).slice(-2); const mm=String(d.getMonth()+1).padStart(2,'0'); const dd=String(d.getDate()).padStart(2,'0'); const ov=document.createElement('div'); ov.className='modal-overlay'; const blobs=[]; const fnames=[]; const urls=[]; const ios=isIOS(); let items=''; for(let i=0;i ${fn} `; } ov.innerHTML=``; document.body.appendChild(ov); requestAnimationFrame(()=>ov.classList.add('open')); // Toggle selection const selected=new Set(); function updateChk(el,idx){ const chk=el.querySelector('.chk'); if(selected.has(idx)){ chk.style.background='#41717D';chk.textContent='✓'; } else { chk.style.background='transparent';chk.textContent=''; } } ov.querySelectorAll('[data-idx]').forEach(el=>{ el.addEventListener('click',()=>{ const idx=parseInt(el.dataset.idx); if(selected.has(idx)) selected.delete(idx); else selected.add(idx); updateChk(el,idx); }); }); ov.querySelector('#selAll').addEventListener('click',()=>{ for(let i=0;iupdateChk(el,parseInt(el.dataset.idx))); }); ov.querySelector('#selNone').addEventListener('click',()=>{ selected.clear(); ov.querySelectorAll('[data-idx]').forEach(el=>updateChk(el,parseInt(el.dataset.idx))); }); // Download selected ov.querySelector('#dlSelected').addEventListener('click',async()=>{ if(selected.size===0)return; const btn=ov.querySelector('#dlSelected'); btn.textContent='Wird gespeichert...';btn.disabled=true; if(ios){ // iOS: share all selected files at once try{ const files=[]; for(const idx of selected){ const buf=await blobs[idx].arrayBuffer(); files.push(new File([buf],fnames[idx],{type:blobs[idx].type,lastModified:Date.now()})); } if(navigator.canShare&&navigator.canShare({files})){ await navigator.share({files}); btn.textContent='✓ Geteilt';btn.style.background='#2E7D32'; return; } }catch(err){ if(err.name==='AbortError'){btn.textContent='Ausgewählte speichern';btn.disabled=false;return;} } btn.textContent='Ausgewählte speichern';btn.disabled=false; } else { // Desktop: trigger downloads sequentially with small delay let i=0; for(const idx of selected){ setTimeout(()=>{ const a=document.createElement('a'); a.href=urls[idx];a.download=fnames[idx]; a.style.display='none'; document.body.appendChild(a);a.click(); setTimeout(()=>document.body.removeChild(a),500); },i*300); i++; } btn.textContent='✓ '+selected.size+' Dateien gespeichert';btn.style.background='#2E7D32'; } }); const close=()=>{ov.classList.remove('open');setTimeout(()=>{ov.remove();urls.forEach(u=>URL.revokeObjectURL(u));},200);}; ov.querySelector('#dlCloseAll').addEventListener('click',close); } // ============================================================ // PDF REPORT EXPORT // ============================================================ async function exportPDF(){ const chapters=[ {id:'projekt', label:'Projekt & Gebäude'}, {id:'huelle', label:'Gebäudehülle'}, {id:'heizung', label:'Heizung'}, {id:'verteilung', label:'Verteilung & HAB'}, {id:'wp', label:'WP / Elektro / FW'}, {id:'splan', label:'Sanierungsfahrplan'}, ]; // Build chip selection modal const chipHtml=chapters.map(c=>`${c.label}`).join(''); const ov=document.createElement('div'); ov.className='modal-overlay'; ov.innerHTML=``; document.body.appendChild(ov); requestAnimationFrame(()=>ov.classList.add('open')); // Toggle chips ov.querySelectorAll('.chip').forEach(c=>c.addEventListener('click',()=>c.classList.toggle('active'))); // Wait for button click const action=await new Promise(res=>{ ov.querySelectorAll('[data-action]').forEach(b=>b.addEventListener('click',()=>res(b.dataset.action))); ov.addEventListener('click',e=>{if(e.target===ov)res('cancel');}); }); const selected=[...ov.querySelectorAll('.chip.active')].map(c=>c.dataset.id); ov.classList.remove('open'); setTimeout(()=>ov.remove(),150); if(action==='cancel'||selected.length===0)return; // Generate print-friendly HTML const addr=load('projekt.adresse')||'Projekt'; const ort=load('projekt.plz_ort')||''; const datum=load('projekt.datum')||''; const allPhotos=await getAllPhotos(); let html=`
`; html+=`

LUT-capture

${addr} · ${ort} · ${datum}
`; function row(label,key){const v=load(key);return v?`
${label}${v}
`:'';} function dynRows(listId){ const tmpl=DYN_TEMPLATES[listId];if(!tmpl)return ''; const count=parseInt(load('_dyn_count_'+listId))||0; let h=''; for(let i=0;i${tmpl.label(i)}`; tmpl.fields.forEach(f=>{ const v=load(listId+'.'+i+'.'+f.key); if(v)h+=`
${f.label}${v}
`; }); // Add photos for this dynamic item h+=photoGrid(listId+'-'+i); } return h; } // Photo grid for report function photoGrid(cat){ const found=allPhotos.filter(p=>p.cat===cat); if(!found.length)return ''; return '
'+found.map(p=>``).join('')+'
'; } if(selected.includes('projekt')){ html+=`

Projekt

`; html+=row('Adresse','projekt.adresse')+row('PLZ / Ort','projekt.plz_ort')+row('Auftraggeber','projekt.auftraggeber'); html+=row('Datum','projekt.datum')+row('Bearbeiter','projekt.bearbeiter')+row('Anwesend','projekt.anwesend'); html+=`

Gebäude

`; html+=row('Baujahr','gebaeude.baujahr')+row('Wohneinheiten','gebaeude.we')+row('Geschosse','gebaeude.geschosse'); html+=row('Wohnfläche m²','gebaeude.wohnflaeche'); html+=row('Gebäudetyp','gebaeude.typ')+row('Denkmalschutz','gebaeude.denkmal')+row('Notiz','gebaeude.notiz'); } if(selected.includes('huelle')){ html+=`

Gebäudehülle

`; html+=dynRows('fassaden'); html+=dynRows('daecher'); html+=dynRows('kellerdecken'); html+=dynRows('fenster'); html+=dynRows('tueren'); } if(selected.includes('heizung')){ html+=`

Heizung

`; html+=photoGrid('revision'); html+=dynRows('warmwasser'); html+=dynRows('verbrauch'); html+=`

Wärmeübergabe

`+row('Heizkörper','uebergabe.hk')+row('FBH','uebergabe.fbh')+row('Elektro','uebergabe.elek')+row('Einzelöfen','uebergabe.oefen'); html+=dynRows('erzeuger')+dynRows('speicher')+dynRows('mag'); html+=`

Abgasführung

`+row('Art','abgas.art')+row('DN','abgas.dn')+row('Notiz','abgas.notiz')+photoGrid('abgas'); html+=`

Heizungsraum

`+row('Länge m','hzraum.l')+row('Breite m','hzraum.b')+row('Zugang','hzraum.zugang'); html+=row('Notiz','hzraum.notiz')+photoGrid('hzraum'); html+=dynRows('deckenhoehen')+dynRows('engstellen'); html+=`

Brennstofflager

`+row('Art','lager.art')+row('Größe','lager.groesse')+row('Notiz','lager.notiz'); } if(selected.includes('verteilung')){ html+=`

Verteilung & HAB

`; html+=row('System','vert.system')+row('Heizkörper','vert.hk')+row('Stränge','vert.straenge'); html+=row('Ventile','vert.ventile')+row('Material','vert.material')+row('Dämmung','vert.daemmung'); html+=row('DN VL','vert.dn_vl')+row('DN RL','vert.dn_rl')+row('Notiz','vert.notiz')+photoGrid('vert'); html+=dynRows('pumpen'); html+=`

Warmwasser-Verteilung

`+row('Zirkulation','ww.zirk')+row('Legionellen','ww.legio')+row('Notiz','ww.notiz'); html+=dynRows('stichproben'); } if(selected.includes('wp')){ html+=`

WP / Elektro / FW

`; html+=`

Aufstellort WP

`+row('Seite','wp.seite')+row('Fläche m²','wp.flaeche')+row('Abstand Nachbar','wp.abstand_n')+row('Abstand Fenster','wp.abstand_f')+row('Gebietstyp','wp.gebiet')+row('Notiz','wp.notiz')+photoGrid('aufstell'); html+=`

Elektroanschluss

`+row('HAK','elek.hak')+row('Zählerplätze','elek.zp_ges')+row('Frei','elek.zp_frei')+row('Querschnitt','elek.querschnitt')+row('PV','elek.pv')+row('PV kWp','elek.pv_kwp')+photoGrid('elek'); html+=`

Fernwärme

`+row('Verfügbarkeit','fw.verfuegbar')+row('Versorger','fw.versorger')+row('Entfernung','fw.entfernung'); } if(selected.includes('splan')){ html+=`

Sanierungsfahrplan

`; ['p1','p2','p3','p4'].forEach((p,i)=>{const v=load('splan.'+p);if(v)html+=`
Paket ${i+1}${v.replace(/\|/g,', ')}
`;}); html+=row('Notiz','splan.notiz'); } html+=photoGrid('allgemein'); html+=`
`; showReport('Bericht – '+addr, html); } // ============================================================ // PDF VIEWER + ANNOTATION // ============================================================ let pdfDoc=null, pdfPageNum=1, pdfTotalPages=0, pdfScale=1.5, pdfPlanIdx=null; let pdfTool=null; // null, 'pen', 'text' let penColor='#E53935', penWidth=3; let pdfAnnotations={}; // {pageNum: [{type,data},...]} let pdfDirty=false; // unsaved annotation changes let pdfAnnotOriginal=null; // snapshot at open for discard let isDrawing=false, drawPoints=[]; function openPdfViewer(planIdx){ pdfPlanIdx=planIdx; pdfPageNum=1; pdfScale=0; // 0 = auto-fit on first render pdfAnnotations={}; document.getElementById('pdfOverlay').classList.add('open'); // Stift standardmäßig aktiv pdfTool='pen'; document.getElementById('btnPen').classList.add('active'); document.getElementById('btnText').classList.remove('active'); document.getElementById('pdfAnnotCanvas').style.pointerEvents='auto'; loadPdfFromDB(planIdx); } async function closePdfViewer(){ if(pdfDirty){ if(await ask('Speichern','Änderungen speichern?','Speichern','Nicht speichern')){ saveAnnotations(); } else { if(!await ask('Verwerfen','Wirklich nicht speichern?','Ja, verwerfen','Zurück')) return; try{pdfAnnotations=JSON.parse(pdfAnnotOriginal)||{};}catch(e){pdfAnnotations={};} saveAnnotations(); } } pdfDirty=false; pdfAnnotOriginal=null; document.getElementById('pdfOverlay').classList.remove('open'); pdfDoc=null; pdfTool=null; document.getElementById('pdfAnnotCanvas').style.pointerEvents='none'; } async function loadPdfFromDB(planIdx){ try{ const photos=await getPhotos('plan-'+planIdx); if(!photos.length){alert('Plan nicht gefunden');closePdfViewer();return;} const dataUrl=photos[0].data; // Load saved annotations try{ const annoPhotos=await getPhotos('anno-'+planIdx); if(annoPhotos.length>0) pdfAnnotations=JSON.parse(annoPhotos[0].data)||{}; else pdfAnnotations={}; }catch(e){pdfAnnotations={};} pdfAnnotOriginal=JSON.stringify(pdfAnnotations); if(dataUrl.startsWith('data:application/pdf')){ if(typeof pdfjsLib==='undefined'){ alert('PDF-Viewer benötigt einmalig Internet beim ersten Öffnen.\n\nBitte kurz WLAN verbinden, Seite neu laden, dann funktioniert es auch offline.'); closePdfViewer();return; } const raw=atob(dataUrl.split(',')[1]); const arr=new Uint8Array(raw.length); for(let i=0;i{ // Auto-fit to viewport width if(pdfScale<=0){ const vpEl=document.getElementById('pdfViewport'); pdfScale=Math.max(0.5,(vpEl.clientWidth-4)/img.width); } const w=Math.round(img.width*pdfScale); const h=Math.round(img.height*pdfScale); cv.width=w;cv.height=h; ctx.drawImage(img,0,0,w,h); setupAnnotCanvas(w,h); redrawAnnotations(); updatePdfPageInfo(); }; img.src=dataUrl; } }catch(e){alert('Fehler: '+e.message);closePdfViewer();} } // Render PDF page to offscreen canvas without touching DOM async function renderPdfOffscreen(){ if(!pdfDoc)return null; const page=await pdfDoc.getPage(pdfPageNum); if(pdfScale<=0){ const vpEl=document.getElementById('pdfViewport'); const testVp=page.getViewport({scale:1}); pdfScale=Math.max(0.5,(vpEl.clientWidth-4)/testVp.width); } const vp=page.getViewport({scale:pdfScale}); const off=document.createElement('canvas'); off.width=vp.width;off.height=vp.height; await page.render({canvasContext:off.getContext('2d'),viewport:vp}).promise; return {canvas:off, w:vp.width, h:vp.height}; } // Apply pre-rendered content to DOM in a single operation function applyRender(result){ if(!result)return; const cv=document.getElementById('pdfRenderCanvas'); const acv=document.getElementById('pdfAnnotCanvas'); const rcv=cv; const wrap=document.getElementById('pdfCanvasWrap'); const {canvas:off, w, h}=result; // All DOM changes in one batch cv.width=w;cv.height=h; cv.getContext('2d').drawImage(off,0,0); acv.width=w;acv.height=h; acv.style.width=rcv.style.width=w+'px'; acv.style.height=rcv.style.height=h+'px'; wrap.style.width=w+'px';wrap.style.height=h+'px'; redrawAnnotations(); updatePdfPageInfo(); } // Convenience: render + apply (for page changes, initial load) async function renderPdfPage(){ const result=await renderPdfOffscreen(); applyRender(result); } function setupAnnotCanvas(w,h){ const acv=document.getElementById('pdfAnnotCanvas'); const rcv=document.getElementById('pdfRenderCanvas'); acv.width=w;acv.height=h; acv.style.width=rcv.style.width=w+'px'; acv.style.height=rcv.style.height=h+'px'; const wrap=document.getElementById('pdfCanvasWrap'); wrap.style.width=w+'px';wrap.style.height=h+'px'; } function updatePdfPageInfo(){ document.getElementById('pdfPageInfo').textContent=pdfPageNum+'/'+pdfTotalPages; } function pdfPrevPage(){if(pdfPageNum>1){saveAnnotations();pdfPageNum--;renderPdfPage();}} function pdfNextPage(){if(pdfPageNum{ let result=null; if(pdfDoc) result=await renderPdfOffscreen(); requestAnimationFrame(()=>{ if(result) applyRender(result); else loadPdfFromDB(pdfPlanIdx); wrap.style.transform=''; wrap.style.transformOrigin='0 0'; vp.scrollLeft=contentX*pdfScale-vp.clientWidth/2; vp.scrollTop=contentY*pdfScale-vp.clientHeight/2; }); })(); } function toggleTool(tool){ const acv=document.getElementById('pdfAnnotCanvas'); if(pdfTool===tool){ pdfTool=null; document.getElementById('btnPen').classList.remove('active'); document.getElementById('btnText').classList.remove('active'); acv.style.pointerEvents='none'; } else { pdfTool=tool; document.getElementById('btnPen').classList.toggle('active',tool==='pen'); document.getElementById('btnText').classList.toggle('active',tool==='text'); acv.style.pointerEvents='auto'; } } const PEN_LABELS={'#E53935':'Heizung','#1565C0':'Hülle','#2E7D32':'Elektro','#000000':'Allgemein'}; function setPenColor(el){ penColor=el.dataset.c; document.querySelectorAll('.pdf-color-dot').forEach(d=>d.classList.remove('active')); el.classList.add('active'); document.getElementById('penLabel').textContent=PEN_LABELS[penColor]||''; } function getAnnotPageKey(){return String(pdfPageNum);} function getAnnotList(){ const k=getAnnotPageKey(); if(!pdfAnnotations[k]) pdfAnnotations[k]=[]; return pdfAnnotations[k]; } function redrawAnnotations(){ const acv=document.getElementById('pdfAnnotCanvas'); const ctx=acv.getContext('2d'); const W=acv.width, H=acv.height; ctx.clearRect(0,0,W,H); const list=getAnnotList(); list.forEach(a=>{ if(a.type==='pen'){ // Normalized coords (0-1) → pixel coords const lw=a.nw?(a.nw*W):a.width; // nw = normalized width ctx.strokeStyle=a.color;ctx.lineWidth=lw;ctx.lineCap='round';ctx.lineJoin='round'; ctx.beginPath(); const pts=a.np||a.points; // np = normalized points pts.forEach((p,i)=>{ const px=a.np?p[0]*W:p[0]; const py=a.np?p[1]*H:p[1]; if(i===0)ctx.moveTo(px,py);else ctx.lineTo(px,py); }); ctx.stroke(); } else if(a.type==='text'){ // Normalized coords → pixel coords const px=a.nx!==undefined?a.nx*W:a.x; const py=a.ny!==undefined?a.ny*H:a.y; const fs=a.nfs?(a.nfs*W):Math.max(16,Math.round(18*pdfScale)); ctx.font='bold '+Math.round(fs)+'px sans-serif'; ctx.fillStyle=a.color; ctx.fillText(a.text,px,py); } }); } function undoAnnotation(){ const list=getAnnotList(); if(list.length>0){list.pop();pdfDirty=true;redrawAnnotations();saveAnnotations();} } async function saveAnnotations(){ if(pdfPlanIdx===null)return; const key='anno-'+pdfPlanIdx; try{ const tx1=db.transaction(DB_STORE,'readwrite'); tx1.objectStore(DB_STORE).delete(key); await new Promise((res,rej)=>{tx1.oncomplete=res;tx1.onerror=rej;}); }catch(e){} const hasAny=Object.keys(pdfAnnotations).some(k=>pdfAnnotations[k].length>0); if(hasAny){ const tx2=db.transaction(DB_STORE,'readwrite'); tx2.objectStore(DB_STORE).put({data:JSON.stringify(pdfAnnotations),cat:key,ts:Date.now()},key); } showSaved(); } // --- Pointer handlers for annotation canvas --- (function(){ const acv=document.getElementById('pdfAnnotCanvas'); // Returns pixel coords on the canvas (for live drawing) function getXY(e){ const r=acv.getBoundingClientRect(); const scaleX=acv.width/r.width, scaleY=acv.height/r.height; return [Math.round((e.clientX-r.left)*scaleX), Math.round((e.clientY-r.top)*scaleY)]; } // Convert pixel coords to normalized (0-1) function normalize(px,py){return [px/acv.width, py/acv.height];} acv.addEventListener('pointerdown',e=>{ if(!pdfTool||pdfTool==='text')return; // text handled via click if(pdfTool==='pen'){ e.preventDefault(); isDrawing=true; drawPoints=[getXY(e)]; acv.setPointerCapture(e.pointerId); } }); // Text tool uses click (fires after pointerup) — reliable keyboard activation on mobile acv.addEventListener('click',e=>{ if(pdfTool!=='text')return; const [x,y]=getXY(e); const wrap=document.getElementById('pdfCanvasWrap'); const r=acv.getBoundingClientRect(); const sX=r.width/acv.width, sY=r.height/acv.height; const inp=document.createElement('input'); inp.className='pdf-text-input'; inp.type='text'; inp.inputMode='text'; inp.style.left=(x*sX)+'px'; inp.style.top=(y*sY-20)+'px'; inp.style.color=penColor; inp.style.fontSize=Math.max(14,Math.round(16*sX))+'px'; inp.placeholder='Text ...'; inp.setAttribute('enterkeyhint','done'); inp.setAttribute('autocapitalize','sentences'); wrap.appendChild(inp); inp.focus(); // synchronous from click = keyboard shows on mobile function commit(){ if(inp.parentNode){ if(inp.value.trim()){ const [nx,ny]=normalize(x,y); const nfs=18/acv.width; getAnnotList().push({type:'text',nx,ny,text:inp.value.trim(),color:penColor,nfs}); pdfDirty=true; redrawAnnotations();saveAnnotations(); } inp.remove(); } } inp.addEventListener('keydown',ev=>{if(ev.key==='Enter')commit();else if(ev.key==='Escape')inp.remove();}); inp.addEventListener('blur',commit); }); acv.addEventListener('pointermove',e=>{ if(!isDrawing)return; e.preventDefault(); drawPoints.push(getXY(e)); // Live preview in pixel coords const ctx=acv.getContext('2d'); redrawAnnotations(); ctx.strokeStyle=penColor;ctx.lineWidth=penWidth;ctx.lineCap='round';ctx.lineJoin='round'; ctx.beginPath(); drawPoints.forEach((p,i)=>{if(i===0)ctx.moveTo(p[0],p[1]);else ctx.lineTo(p[0],p[1]);}); ctx.stroke(); }); acv.addEventListener('pointerup',e=>{ if(!isDrawing)return; isDrawing=false; if(drawPoints.length>1){ // Convert pixel points to normalized (0-1) for storage const np=drawPoints.map(p=>normalize(p[0],p[1])); const nw=penWidth/acv.width; // normalized pen width getAnnotList().push({type:'pen',np,nw,color:penColor}); pdfDirty=true; saveAnnotations(); } drawPoints=[]; redrawAnnotations(); }); acv.addEventListener('pointercancel',()=>{isDrawing=false;drawPoints=[];}); })(); // --- Pinch-to-zoom + pan for PDF viewer --- (function(){ const vp=document.getElementById('pdfViewport'); const wrap=document.getElementById('pdfCanvasWrap'); let isPinching=false; let pinchStartDist=0, pinchStartScale=1, lastDist=0; let startMidX=0, startMidY=0; // initial midpoint (viewport coords) let curMidX=0, curMidY=0; // current midpoint let originX=0, originY=0; // transform-origin on wrap let startScrollX=0, startScrollY=0; // scroll position at pinch start const touches=new Map(); let renderTimer=null; function dist2(){ const pts=[...touches.values()]; if(pts.length<2)return 0; const dx=pts[0].x-pts[1].x, dy=pts[0].y-pts[1].y; return Math.sqrt(dx*dx+dy*dy); } function midPt(){ const pts=[...touches.values()]; if(pts.length<2)return{x:0,y:0}; return{x:(pts[0].x+pts[1].x)/2, y:(pts[0].y+pts[1].y)/2}; } vp.addEventListener('touchstart',e=>{ for(const t of e.changedTouches) touches.set(t.identifier,{x:t.clientX,y:t.clientY}); if(touches.size===2&&!isPinching){ isPinching=true; pinchStartDist=lastDist=dist2(); pinchStartScale=pdfScale; isDrawing=false;drawPoints=[]; const m=midPt(); startMidX=curMidX=m.x; startMidY=curMidY=m.y; startScrollX=vp.scrollLeft; startScrollY=vp.scrollTop; // Transform-origin = where fingers are on the wrap const wr=wrap.getBoundingClientRect(); originX=m.x-wr.left;originY=m.y-wr.top; wrap.style.transformOrigin=originX+'px '+originY+'px'; e.preventDefault(); } },{passive:false}); vp.addEventListener('touchmove',e=>{ for(const t of e.changedTouches){ if(touches.has(t.identifier)) touches.set(t.identifier,{x:t.clientX,y:t.clientY}); } if(isPinching&&touches.size>=2){ e.preventDefault(); lastDist=dist2(); const m=midPt(); curMidX=m.x;curMidY=m.y; // Pan delta (how much fingers moved as a group) const panDx=curMidX-startMidX; const panDy=curMidY-startMidY; // Scale ratio const ratio=pinchStartDist>0?lastDist/pinchStartDist:1; const clamped=Math.max(0.5/pinchStartScale, Math.min(5/pinchStartScale, ratio)); // Visual preview only via CSS transform (scroll applied on release) wrap.style.transform='translate('+panDx+'px,'+panDy+'px) scale('+clamped+')'; } },{passive:false}); function endPinch(){ if(!isPinching||pinchStartDist<=0){isPinching=false;touches.clear();return;} const ratio=lastDist/pinchStartDist; const newScale=Math.max(0.5,Math.min(5,pinchStartScale*ratio)); const vpR=vp.getBoundingClientRect(); // Content point under initial midpoint (in base page coords, scale=1) const initVpX=startMidX-vpR.left; const initVpY=startMidY-vpR.top; const contentX=(startScrollX+initVpX)/pinchStartScale; const contentY=(startScrollY+initVpY)/pinchStartScale; // Where fingers ended in viewport const endVpX=curMidX-vpR.left; const endVpY=curMidY-vpR.top; const scaleChanged=Math.abs(newScale-pinchStartScale)>0.02; if(!scaleChanged){ // Pure pan — reset transform, apply scroll offset wrap.style.transform=''; wrap.style.transformOrigin='0 0'; vp.scrollLeft=contentX*pinchStartScale-endVpX; vp.scrollTop=contentY*pinchStartScale-endVpY; isPinching=false;pinchStartDist=0;touches.clear(); return; } // Scale changed — keep CSS transform while rendering offscreen pdfScale=newScale; isPinching=false;pinchStartDist=0;touches.clear(); clearTimeout(renderTimer); renderTimer=setTimeout(async()=>{ let result=null; if(pdfDoc){ result=await renderPdfOffscreen(); } // All DOM changes in one single frame: apply content + remove transform + set scroll requestAnimationFrame(()=>{ if(result){ applyRender(result); } else { // Image path — re-render via loadPdfFromDB handled separately loadPdfFromDB(pdfPlanIdx); } wrap.style.transform=''; wrap.style.transformOrigin='0 0'; vp.scrollLeft=contentX*pdfScale-endVpX; vp.scrollTop=contentY*pdfScale-endVpY; }); },20); } vp.addEventListener('touchend',e=>{ if(isPinching){ // Update touch positions before ending for(const t of e.changedTouches) touches.delete(t.identifier); if(touches.size<2) endPinch(); } else { for(const t of e.changedTouches) touches.delete(t.identifier); } }); vp.addEventListener('touchcancel',e=>{ for(const t of e.changedTouches) touches.delete(t.identifier); if(isPinching&&touches.size<2) endPinch(); }); })(); // ============================================================ // EXPORT / IMPORT // ============================================================ async function exportJSON(draft=false){ saveRecentProject(); const data={_meta:{app:'LUT-capture',version:'2.0',exported:new Date().toISOString()}}; // Dump ALL localStorage items with our prefix – no filtering const ls={}; for(let i=0;i0){ data._photos=photos.map(p=>({key:p.key,cat:p.cat,data:p.data})); } }catch(e){} const addr=ls['projekt.adresse']||'Capture'; const name=addr.replace(/[^a-zA-Z0-9äöüÄÖÜß\-_]/g,'_').replace(/_+/g,'_').substring(0,30); const d=new Date(); const yy=String(d.getFullYear()).slice(-2); const mm=String(d.getMonth()+1).padStart(2,'0'); const dd=String(d.getDate()).padStart(2,'0'); const device=isMobile()?'mobil':'pc'; const suffix=draft?'_ENTWURF':''; const fn='json_'+yy+mm+dd+'_'+name+'_'+device+suffix+'.json'; const jsonStr=JSON.stringify(data); const blob=new Blob([jsonStr],{type:'application/json'}); offerDownload(fn, blob); } // Show download-ready modal with real clickable link function isIOS(){return /iPad|iPhone|iPod/.test(navigator.userAgent)||(/Macintosh/.test(navigator.userAgent)&&'ontouchend' in document);} function offerDownload(filename, blob){ const ov=document.createElement('div'); ov.className='modal-overlay'; if(isIOS()){ // Create blob URL for fallback const blobUrl=URL.createObjectURL(blob); ov.innerHTML=``; document.body.appendChild(ov); requestAnimationFrame(()=>ov.classList.add('open')); const close=()=>{ov.classList.remove('open');setTimeout(()=>{ov.remove();URL.revokeObjectURL(blobUrl);},500);}; const hint=ov.querySelector('#dlHint'); // "In Dateien sichern" — opens file in Safari tab, user uses Safari share → In Dateien ov.querySelector('#dlShareFiles').addEventListener('click',()=>{ window.open(blobUrl,'_blank'); hint.style.display='block'; hint.innerHTML='Datei öffnet sich in neuem Tab.
Dort: Teilen ⬆„In Dateien sichern"
→ OneDrive-Ordner wählen'; }); // "Teilen" — native share sheet for AirDrop, Mail, etc. ov.querySelector('#dlShareApps').addEventListener('click',async()=>{ try{ const buf=await blob.arrayBuffer(); // Use text/plain as MIME — iOS shows more targets for text files const shareType=filename.endsWith('.json')?'text/plain':blob.type; const file=new File([buf],filename,{type:shareType,lastModified:Date.now()}); if(navigator.canShare&&navigator.canShare({files:[file]})){ await navigator.share({files:[file]}); ov.querySelector('#dlShareApps').textContent='✓ Geteilt'; ov.querySelector('#dlShareApps').style.background='#c8e6c9'; return; } }catch(err){ if(err.name==='AbortError') return; } hint.style.display='block'; hint.textContent='Teilen nicht verfügbar auf diesem Gerät.'; }); ov.querySelector('#dlClose').addEventListener('click',close); ov.addEventListener('click',e=>{if(e.target===ov)close();}); } else { const url=URL.createObjectURL(blob); ov.innerHTML=``; document.body.appendChild(ov); requestAnimationFrame(()=>ov.classList.add('open')); const close=()=>{ov.classList.remove('open');setTimeout(()=>{ov.remove();URL.revokeObjectURL(url);},200);}; ov.querySelector('#dlLink').addEventListener('click',()=>setTimeout(close,500)); ov.querySelector('#dlClose').addEventListener('click',close); ov.addEventListener('click',e=>{if(e.target===ov)close();}); } } async function importJSON(e){ const f=e.target.files[0];if(!f)return; if(!await ask('Projekt laden','Bestehende Erfassung wird überschrieben. Fortfahren?','Laden','Abbrechen'))return; const r=new FileReader(); r.onload=async ev=>{ try{ const data=JSON.parse(ev.target.result); // Clear existing localStorage clearLocalStorage(); // Restore all localStorage items if(data._localStorage){ Object.keys(data._localStorage).forEach(k=>{ try{localStorage.setItem(LS_KEY+k,data._localStorage[k]);}catch(e){} }); } // Restore photos to IndexedDB if(data._photos&&data._photos.length>0){ const tx=db.transaction(DB_STORE,'readwrite'); const store=tx.objectStore(DB_STORE); store.clear(); data._photos.forEach(p=>{store.put({data:p.data,cat:p.cat,ts:Date.now()},p.key);}); tx.oncomplete=()=>{sessionStorage.setItem('lut_auto_resume','1');location.reload();}; } else { sessionStorage.setItem('lut_auto_resume','1');location.reload(); } }catch(err){alert('Datei konnte nicht geladen werden: '+err.message);} }; r.readAsText(f); e.target.value=''; } function clearLocalStorage(){ const toRemove=[]; for(let i=0;ilocalStorage.removeItem(k)); } async function resetAll(){ if(!await ask('Zurücksetzen','ALLE Daten dieser Erfassung löschen?','Löschen','Abbrechen'))return; if(!await ask('Sicher?','Dies kann nicht rückgängig gemacht werden.','Ja, alles löschen','Zurück'))return; clearLocalStorage(); if(db){ const tx=db.transaction(DB_STORE,'readwrite'); tx.objectStore(DB_STORE).clear(); tx.oncomplete=()=>location.reload(); } else location.reload(); } // ============================================================ // INIT // ============================================================ async function init(){ await openDB(); bindInputs(); updateSplanVisibility(); restoreDynLists(); await restoreAllPhotos(); renderPlanList(); updatePhotoCounts(); // Set today as default date const dateInp=document.querySelector('[data-key="projekt.datum"]'); if(dateInp&&!dateInp.value){ dateInp.value=new Date().toISOString().split('T')[0]; save('projekt.datum',dateInp.value); } // Update header when address changes const addrInp=document.querySelector('[data-key="projekt.adresse"]'); if(addrInp){ addrInp.addEventListener('input',()=>{ document.querySelector('.hdr-title').textContent=addrInp.value||'Neues Projekt'; }); } // Show start screen or auto-resume renderRecentProjects(); initApiKeyField(); if(sessionStorage.getItem('lut_auto_resume')==='1'){ sessionStorage.removeItem('lut_auto_resume'); showSec('projekt'); } // Start screen is already active by default via HTML } init(); // === KONTEXTHILFE === (function(){ const HELP={ 'vert.dn_vl':{ title:'DN Vorlauf messen', text:'Rohraußendurchmesser mit Schieblehre oder Maßband messen. Umfang durch 3,14 teilen = Außen-DN. Typische Werte: DN 20 (≈27mm), DN 25 (≈34mm), DN 32 (≈42mm), DN 40 (≈48mm), DN 50 (≈60mm). Am Verteiler oder in Nähe des Kessels messen, wo die Rohre frei liegen.' }, 'vert.dn_rl':{ title:'DN Rücklauf messen', text:'Wie Vorlauf — am besten direkt daneben messen. VL und RL haben meist gleichen DN. Falls unterschiedlich, beide Werte notieren.' }, 'vert.daemmung':{ title:'Rohrdämmung beurteilen', text:'Prüfen: Sind Heizleitungen im Keller gedämmt? EnEV/GEG fordert 100% Dämmung der zugänglichen Leitungen. "Ja" = vollständig gedämmt, "Teilw." = Lücken oder nur Hauptleitungen, "Nein" = weitgehend ungedämmt. Auf Zustand achten: alte Glaswolle-Schalen zählen als "Teilw."' }, 'vert.ventile':{ title:'Thermostatventile beurteilen', text:'"Mit VE" = voreinstellbar (erkennbar an Zahlenring/Skala am Ventilunterteil, z.B. Oventrop, Danfoss RA-N). "Ohne VE" = nicht voreinstellbar (alter Standard, Ventiloberteil direkt auf Gehäuse). Bei Mischbestand die Mehrheit angeben, im Notizfeld Details ergänzen.' }, 'abgas.dn':{ title:'Abgasrohr DN ermitteln', text:'Rohraußenumfang mit flexiblem Maßband messen und hier den nächsten passenden Wert wählen. Bei gemauerten Schornsteinen: lichte Weite der Öffnung messen. Tipp: Kesseldatenblatt gibt DN oft direkt an.' }, 'hzraum.l':{ title:'Heizraum ausmessen', text:'Innenmaß Länge des Heizraums in Metern. Wichtig für WP-Aufstellung: Luft-Wasser-WP innen braucht ca. 2×1m Stellfläche + Wartungszugang. Auch für Pufferspeicher und Hydraulik-Anschlüsse relevant.' }, 'lager.art':{ title:'Brennstofflager identifizieren', text:'Gasanschluss: gelbes Rohr mit HAE (Hauptabsperreinrichtung), Zähler. Öltank Keller: Stahltank oder Kunststoff-Batterie, Tankgröße am Typenschild. Erdtank: Domschacht im Garten, Befüllstutzen außen. Wichtig für Rückbaukosten und Platzgewinn.' }, 'vert.straenge':{ title:'Heizungsstränge zählen', text:'Anzahl der senkrechten Rohrleitungsstränge, die vom Verteiler im Keller nach oben gehen. Am einfachsten am Heizungsverteiler abzählen. Wichtig für hydraulischen Abgleich und Aufwandsschätzung Ventiltausch.' }, 'vert.hk':{ title:'Heizkörper schätzen', text:'Gesamtzahl aller Heizkörper im Gebäude. Schnellschätzung: WE × Räume/WE × HK/Raum. Typisch MFH: 4–6 HK pro WE. Exakte Zählung über LUT-rooms. Relevant für: Ventiltausch-Kosten, hydraulischer Abgleich.' }, 'ww.zirk':{ title:'Zirkulation prüfen', text:'Zirkulationsleitung = warmes Wasser kommt sofort an jeder Zapfstelle. Erkennbar an: Rücklaufleitung (3. Rohr zum Speicher), Zirkulationspumpe am Speicher, Zeitschaltuhr. Ohne Zirkulation: Wasser läuft kalt, bis Warmwasser nachkommt. Relevant für Legionellenschutz und Komfortbewertung.' } }; // Inject help buttons Object.keys(HELP).forEach(key=>{ const el=document.querySelector('[data-key="'+key+'"]'); if(!el)return; const field=el.closest('.field'); if(!field)return; const label=field.querySelector('.label'); if(!label)return; const btn=document.createElement('button'); btn.type='button'; btn.className='help-btn'; btn.textContent='?'; btn.dataset.helpKey=key; label.appendChild(btn); }); // Create overlay const ov=document.createElement('div'); ov.id='helpOverlay'; ov.innerHTML='
Foto folgt
'; document.body.appendChild(ov); // Event delegation for help buttons document.addEventListener('click',(e)=>{ const btn=e.target.closest('.help-btn'); if(!btn)return; e.preventDefault(); e.stopPropagation(); const key=btn.dataset.helpKey; const h=HELP[key]; if(!h)return; document.getElementById('helpTitle').textContent=h.title; document.getElementById('helpText').textContent=h.text; const imgEl=document.getElementById('helpImg'); if(h.img){ imgEl.innerHTML=''; } else { imgEl.innerHTML='
Referenzfoto wird nachgereicht
'; } ov.classList.add('show'); }); document.getElementById('helpClose').onclick=()=>ov.classList.remove('show'); ov.addEventListener('click',(e)=>{if(e.target===ov)ov.classList.remove('show');}); })(); // === FAB Quick-Foto (Speed-Optimized) === (function(){ const CATS=[ {id:'allgemein',label:'Allgemein',color:'#8E8D85'}, {id:'hzraum',label:'Heizraum',color:'#B8562A'}, {id:'aufstell',label:'Aufstellort',color:'#2E6B9C'}, {id:'elek',label:'Elektro',color:'#C4841D'}, {id:'vert',label:'Keller/Stränge',color:'#1A7A5A'}, {id:'fassade',label:'Fassade',color:'#7B4FA0'}, ]; const AUTOSAVE_MS=2000; /* --- FAB button + category menu --- */ const wrap=document.createElement('div'); wrap.className='fab-wrap'; wrap.innerHTML= '
'+ CATS.map(c=>'').join('')+ '
'+ ''; document.body.appendChild(wrap); /* --- Quick-Toast HTML --- */ const toast=document.createElement('div'); toast.id='quickToast'; toast.innerHTML= '
'+ ''+ '
'+ 'Speichern in 2s\u2026'+ 'Allgemein'+ '
'+ '
'+ ''+ ''+ '
'+ '
'+ '
'+ '
'+ ''+ ''+ '
'+ '
'+ CATS.map(c=>'').join('')+ '
'; document.body.appendChild(toast); const fab=document.getElementById('fabBtn'); const cats=document.getElementById('fabCats'); let catsOpen=false; /* --- FAB: Tap = direkt Kamera, Long-Press = Kategorie-Menü --- */ let pressTimer=null, didLongPress=false; fab.addEventListener('touchstart',()=>{ didLongPress=false; pressTimer=setTimeout(()=>{ didLongPress=true; pressTimer=null; cats.classList.add('open');catsOpen=true; },400); },{passive:true}); fab.addEventListener('touchend',(e)=>{ if(pressTimer){clearTimeout(pressTimer);pressTimer=null;} if(!didLongPress&&!catsOpen){ e.preventDefault(); openCamera('allgemein'); } }); fab.addEventListener('click',(e)=>{ // Desktop: close cats if open, else direct capture if(catsOpen){cats.classList.remove('open');catsOpen=false;return;} if(!('ontouchstart' in window)) openCamera('allgemein'); }); // Category button click (long-press menu) cats.addEventListener('click',(e)=>{ const btn=e.target.closest('.fab-cat'); if(!btn)return; cats.classList.remove('open');catsOpen=false; openCamera(btn.dataset.cat); }); // Close cats on outside tap document.addEventListener('click',(e)=>{ if(catsOpen&&!wrap.contains(e.target)){cats.classList.remove('open');catsOpen=false;} }); /* --- Camera open + compress --- */ function openCamera(cat){ const inp=document.createElement('input'); inp.type='file';inp.accept='image/*';inp.capture='environment'; inp.onchange=e=>{ const f=e.target.files[0];if(!f)return; const reader=new FileReader(); reader.onload=ev=>{ const img=new Image(); img.onload=()=>{ const MAX=2000;let w=img.width,h=img.height; if(w>MAX||h>MAX){const s=MAX/Math.max(w,h);w=Math.round(w*s);h=Math.round(h*s);} const cv=document.createElement('canvas');cv.width=w;cv.height=h; cv.getContext('2d').drawImage(img,0,0,w,h); const compressed=cv.toDataURL('image/jpeg',0.85); showQuickToast(compressed,cat); }; img.src=ev.target.result; }; reader.readAsDataURL(f); }; inp.click(); } /* --- Quick-Toast State --- */ let qtTimer=null, qtData=null, qtCat=null; const qtThumb=document.getElementById('qtThumb'); const qtLabel=document.getElementById('qtLabel'); const qtCatChip=document.getElementById('qtCatChip'); const qtBar=document.getElementById('qtBar'); const qtNoteRow=document.getElementById('qtNoteRow'); const qtNoteInput=document.getElementById('qtNoteInput'); const qtCatPick=document.getElementById('qtCatPick'); function catLabel(id){const c=CATS.find(x=>x.id===id);return c?c.label:id;} function showQuickToast(dataUrl,cat){ // Reset state cancelQuickSave(true); qtData=dataUrl;qtCat=cat; qtThumb.src=dataUrl; qtCatChip.textContent=catLabel(cat); qtLabel.textContent='Speichern in 2s\u2026'; qtNoteRow.classList.remove('open'); qtNoteInput.value=''; qtCatPick.classList.remove('open'); // Reset bar then animate qtBar.style.transition='none'; qtBar.style.width='100%'; requestAnimationFrame(()=>{ requestAnimationFrame(()=>{ qtBar.style.transition='width '+AUTOSAVE_MS+'ms linear'; qtBar.style.width='0%'; }); }); // Show toast toast.style.display='flex'; requestAnimationFrame(()=>toast.classList.add('show')); // Start countdown qtTimer=setTimeout(()=>confirmQuickSave(),AUTOSAVE_MS); } function hideToast(){ toast.classList.add('hiding'); setTimeout(()=>{ toast.classList.remove('show','hiding'); toast.style.display='none'; },200); } function confirmQuickSave(){ if(qtTimer){clearTimeout(qtTimer);qtTimer=null;} if(!qtData)return; const note=qtNoteInput.value.trim(); savePhoto(qtCat,qtData,note||undefined); qtData=null; hideToast(); } function cancelQuickSave(silent){ if(qtTimer){clearTimeout(qtTimer);qtTimer=null;} qtData=null; if(!silent)hideToast(); } /* --- Toast button handlers --- */ document.getElementById('qtCancel').addEventListener('click',()=>cancelQuickSave()); document.getElementById('qtNote').addEventListener('click',()=>{ if(qtTimer){clearTimeout(qtTimer);qtTimer=null;} qtBar.style.transition='none';qtBar.style.width='0%'; qtLabel.textContent='Anmerkung eingeben'; qtNoteRow.classList.add('open'); qtCatPick.classList.remove('open'); setTimeout(()=>qtNoteInput.focus(),60); }); document.getElementById('qtNoteSave').addEventListener('click',()=>confirmQuickSave()); qtNoteInput.addEventListener('keydown',(e)=>{if(e.key==='Enter'){e.preventDefault();confirmQuickSave();}}); /* --- Category re-pick inside toast --- */ qtCatChip.addEventListener('click',()=>{ if(qtTimer){clearTimeout(qtTimer);qtTimer=null;} qtBar.style.transition='none';qtBar.style.width='0%'; qtLabel.textContent='Kategorie w\u00e4hlen'; qtCatPick.classList.toggle('open'); qtNoteRow.classList.remove('open'); // Highlight current qtCatPick.querySelectorAll('button').forEach(b=>b.classList.toggle('sel',b.dataset.cat===qtCat)); }); qtCatPick.addEventListener('click',(e)=>{ const btn=e.target.closest('button[data-cat]');if(!btn)return; qtCat=btn.dataset.cat; qtCatChip.textContent=catLabel(qtCat); qtCatPick.classList.remove('open'); qtLabel.textContent='Speichern in 2s\u2026'; // Re-start countdown qtBar.style.transition='none';qtBar.style.width='100%'; requestAnimationFrame(()=>{requestAnimationFrame(()=>{ qtBar.style.transition='width '+AUTOSAVE_MS+'ms linear'; qtBar.style.width='0%'; });}); qtTimer=setTimeout(()=>confirmQuickSave(),AUTOSAVE_MS); }); })(); // PWA Service Worker if('serviceWorker' in navigator){ navigator.serviceWorker.register('LUT-capture_sw.js').catch(()=>{}); }