From b6e726fce5461435444b426b28d167692d112710 Mon Sep 17 00:00:00 2001 From: Till Heidrich Date: Mon, 13 Jul 2026 08:50:30 +0200 Subject: [PATCH] Export: sauberes Logo, korrektes Layout, direkter PDF-Download ohne Druckdialog; Astrid-Name entfernt --- index.html | 107 ++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 74 insertions(+), 33 deletions(-) diff --git a/index.html b/index.html index cf05a8d..f2d42d8 100644 --- a/index.html +++ b/index.html @@ -288,7 +288,7 @@ summary{font-size:12.5px;color:var(--sea2);cursor:pointer;padding:4px 0}

Momentaufnahme deiner Getränkepaket-Abrechnung sichern.

- +
@@ -610,50 +610,91 @@ $("#resetAll").addEventListener("click",()=>{if(confirm("Wirklich alles zurücks try{localStorage.removeItem(KEY);}catch(e){} S={pkg:"fun",priceByPkg:{fun:159,allin:279,kfun:97,kallin:125},persons:2,days:7,curday:1,byday:{},kid:{},priceOv:{},editPrices:false,custom:[]}; $("#editPrices").checked=false;$("#priceResetBar").style.display="none";initForm();renderDrinks();renderKid();compute();}}); -$("#expPdf").addEventListener("click",()=>window.print()); +$("#expPdf").addEventListener("click",exportPdf); $("#expImg").addEventListener("click",exportImage); -/* ---- JPG-Export (Canvas, offline) ---- */ -function exportImage(){ +/* ---- Abrechnung als Canvas rendern (offline) ---- */ +function buildReceipt(){ const L=LAST,W=760,pad=44,dpr=2; + const y0=142, step=40, chartTop=356, ch=120; + const barTop=chartTop+18, labelY=barTop+ch+15, footY=labelY+34, H=footY+18; const cv=document.createElement("canvas"); - const rowsTop=250, chartH=150, foot=70; - const H=rowsTop+chartH+foot+40; cv.width=W*dpr;cv.height=H*dpr;const g=cv.getContext("2d");g.scale(dpr,dpr); - g.fillStyle="#edf2f5";g.fillRect(0,0,W,H); - // header - g.fillStyle="#0b3a54";g.fillRect(0,0,W,92); - g.fillStyle="#e2673d";g.beginPath();g.moveTo(pad,30);g.lineTo(pad+30,30);g.lineTo(pad+15,48);g.lineTo(pad+15,58);g.lineTo(pad+22,58);g.lineTo(pad+22,63);g.lineTo(pad+8,63);g.lineTo(pad+8,58);g.lineTo(pad+15,58);g.closePath();g.fill(); - g.fillStyle="#fff";g.font="700 22px "+FF();g.fillText("Getränkepaket-Abrechnung",pad+46,40); - g.fillStyle="#cfe0ea";g.font="13px "+FF(); - g.fillText(PKGNAME[L.pkg]+" · "+L.persons+"× · Tag "+L.curday+" von "+L.days+" · "+new Date().toLocaleDateString("de-DE"),pad+46,64); - // rows - let y=132;g.textBaseline="alphabetic"; - const line=(lab,val,col)=>{g.fillStyle="#5f7480";g.font="14px "+FF();g.fillText(lab,pad,y); - g.fillStyle=col||"#151b1f";g.font="700 18px "+MF();g.textAlign="right";g.fillText(val,W-pad,y);g.textAlign="left";y+=38;}; - line("Paketkosten gesamt",eur(L.pkgTotal)); - line("Angerechneter Wert (im Paket)",eur(L.consumed),"#12556f"); + g.textBaseline="alphabetic"; + g.fillStyle="#eef3f6";g.fillRect(0,0,W,H); + // header bar + g.fillStyle="#0b3a54";g.fillRect(0,0,W,96); + // sauberes Martini-Logo + const lx=pad,ly=30; + g.fillStyle="#e2673d"; + g.beginPath();g.moveTo(lx,ly);g.lineTo(lx+30,ly);g.lineTo(lx+15,ly+18);g.closePath();g.fill(); // Kelch + g.fillRect(lx+13.5,ly+18,3,15); // Stiel + g.fillRect(lx+5,ly+33,20,3); // Fuß + g.beginPath();g.arc(lx+24,ly+3,2.6,0,7);g.fillStyle="#9fd0e6";g.fill(); // Olive + // Titel + g.fillStyle="#ffffff";g.font="700 23px "+FF();g.fillText("Getränkepaket-Abrechnung",lx+48,42); + g.fillStyle="#bcd4e2";g.font="13px "+FF(); + g.fillText(PKGNAME[L.pkg]+" · "+L.persons+"× · Tag "+L.curday+" von "+L.days+" · "+new Date().toLocaleDateString("de-DE"),lx+48,67); + // Zeilen + let y=y0; + const line=(lab,val,col,sub)=>{ + g.fillStyle="#33454f";g.font="15px "+FF();g.fillText(lab,pad,y); + g.fillStyle=col||"#151b1f";g.font="700 19px "+MF();g.textAlign="right";g.fillText(val,W-pad,y);g.textAlign="left"; + if(sub){g.fillStyle="#8a9aa4";g.font="12px "+FF();g.fillText(sub,pad,y+16);} + g.strokeStyle="#dbe5eb";g.beginPath();g.moveTo(pad,y+ (sub?24:11));g.lineTo(W-pad,y+(sub?24:11));g.stroke(); + y+=step+(sub?12:0);}; const done=L.remaining<=0.001; - line(done?"Break-even – Plus":"Noch bis Break-even",done?"+"+eur(-L.remaining):eur(L.remaining),done?"#1f9257":"#c2352a"); + line("Paketkosten gesamt",eur(L.pkgTotal),"#0b3a54"); + line("Angerechneter Wert (im Paket)",eur(L.consumed),"#12556f"); + line(done?"Break-even erreicht – Plus":"Noch bis Break-even",done?"+"+eur(-L.remaining):eur(L.remaining),done?"#1f9257":"#c2352a","Break-even pro Tag: "+eur(L.perDay)); line("Zusatzkosten (nicht im Paket)",eur(L.extra),"#c9542e"); line("Gratis-Bonus Kind unter 4",eur(L.kidVal),"#1f9257"); - // chart - const cx=pad, cy=rowsTop+18, cw=W-2*pad, ch=chartH-30; - g.fillStyle="#5f7480";g.font="12px "+FF();g.fillText("Angerechneter Wert je Tag (Ziel "+eur(L.perDay)+")",cx,cy-8); + // Chart + const cx=pad,cw=W-2*pad; + g.fillStyle="#5f7480";g.font="12px "+FF();g.fillText("Angerechneter Wert je Tag · Ziel "+eur(L.perDay)+"/Tag",cx,chartTop+4); const scale=Math.max(L.perDay*1.25,Math.max.apply(null,L.vals.concat([1]))*1.1,1); - const n=L.vals.length,bw=Math.min(30,(cw/n)-4); - // target line - const ty=cy+ch-(L.perDay/scale*ch);g.strokeStyle="#e2673d";g.setLineDash([5,4]);g.beginPath();g.moveTo(cx,ty);g.lineTo(cx+cw,ty);g.stroke();g.setLineDash([]); - for(let i=0;i0?3:0); - const bx=cx+i*(cw/n)+((cw/n)-bw)/2, by=cy+ch-bh; + const n=L.vals.length,slot=cw/n,bw=Math.min(28,slot-6); + const ty=barTop+ch-(L.perDay/scale*ch); + g.strokeStyle="#e2673d";g.setLineDash([5,4]);g.beginPath();g.moveTo(cx,ty);g.lineTo(cx+cw,ty);g.stroke();g.setLineDash([]); + for(let i=0;i0?3:0),bx=cx+i*slot+(slot-bw)/2,by=barTop+ch-bh; g.fillStyle=(i+1===L.bestDay&&v>0)?"#1f9257":(v>=L.perDay&&v>0?"#2e8f5c":"#12556f"); g.fillRect(bx,by,bw,bh); - g.fillStyle="#9fb3bf";g.font="9px "+MF();g.textAlign="center";g.fillText(i+1,bx+bw/2,cy+ch+13);g.textAlign="left";} - // footer + g.fillStyle="#9fb3bf";g.font="10px "+MF();g.textAlign="center";g.fillText(i+1,bx+bw/2,barTop+ch+14);g.textAlign="left";} + // Footer g.fillStyle="#5f7480";g.font="12px "+FF(); - g.fillText("aidadrinks.heidrich-digital.de · Preisstand AIDA-Barkarte 01.11.2025 · inoffiziell",pad,H-26); - cv.toBlob(b=>{const u=URL.createObjectURL(b),a=document.createElement("a"); - a.href=u;a.download="Getraenkepaket-Abrechnung.jpg";a.click();setTimeout(()=>URL.revokeObjectURL(u),4000);},"image/jpeg",0.92); + g.fillText("aidadrinks.heidrich-digital.de · Preisstand AIDA-Barkarte 01.11.2025 · inoffizieller Helfer",pad,footY); + return{cv,W,H}; +} +function download(blob,name){const u=URL.createObjectURL(blob),a=document.createElement("a"); + a.href=u;a.download=name;a.click();setTimeout(()=>URL.revokeObjectURL(u),4000);} +function exportImage(){const{cv}=buildReceipt();cv.toBlob(b=>download(b,"Getraenkepaket-Abrechnung.jpg"),"image/jpeg",0.92);} +/* echtes PDF ohne Druckdialog: JPG in minimales 1-Seiten-PDF verpacken (offline, keine Library) */ +function exportPdf(){ + const{cv,W,H}=buildReceipt(); + cv.toBlob(async b=>{ + const jpg=new Uint8Array(await b.arrayBuffer()); + download(jpgToPdf(jpg,W,H),"Getraenkepaket-Abrechnung.pdf"); + },"image/jpeg",0.92); +} +function jpgToPdf(jpg,w,h){ + w=Math.round(w);h=Math.round(h); + const enc=new TextEncoder(),chunks=[];let len=0;const off={}; + const push=u=>{chunks.push(u);len+=u.length;}; + const s=t=>push(enc.encode(t)); + const start=n=>{off[n]=len;s(n+" 0 obj\n");}; + s("%PDF-1.3\n"); + start(1);s("<< /Type /Catalog /Pages 2 0 R >>\nendobj\n"); + start(2);s("<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n"); + start(3);s("<< /Type /Page /Parent 2 0 R /MediaBox [0 0 "+w+" "+h+"] /Resources << /XObject << /Im0 4 0 R >> >> /Contents 5 0 R >>\nendobj\n"); + start(4);s("<< /Type /XObject /Subtype /Image /Width "+(w*2)+" /Height "+(h*2)+" /ColorSpace /DeviceRGB /BitsPerComponent 8 /Filter /DCTDecode /Length "+jpg.length+" >>\nstream\n"); + push(jpg);s("\nendstream\nendobj\n"); + const content="q "+w+" 0 0 "+h+" 0 0 cm /Im0 Do Q"; + start(5);s("<< /Length "+content.length+" >>\nstream\n"+content+"\nendstream\nendobj\n"); + const xref=len;let x="xref\n0 6\n0000000000 65535 f \n"; + for(let i=1;i<=5;i++)x+=String(off[i]).padStart(10,"0")+" 00000 n \n"; + s(x);s("trailer\n<< /Size 6 /Root 1 0 R >>\nstartxref\n"+xref+"\n%%EOF"); + const out=new Uint8Array(len);let o=0;chunks.forEach(c=>{out.set(c,o);o+=c.length;}); + return new Blob([out],{type:"application/pdf"}); } function FF(){return '-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Arial,sans-serif';} function MF(){return 'ui-monospace,Menlo,Consolas,monospace';}