Export: sauberes Logo, korrektes Layout, direkter PDF-Download ohne Druckdialog; Astrid-Name entfernt
This commit is contained in:
+74
-33
@@ -288,7 +288,7 @@ summary{font-size:12.5px;color:var(--sea2);cursor:pointer;padding:4px 0}
|
|||||||
<p class="hint" style="margin-top:0">Momentaufnahme deiner Getränkepaket-Abrechnung sichern.</p>
|
<p class="hint" style="margin-top:0">Momentaufnahme deiner Getränkepaket-Abrechnung sichern.</p>
|
||||||
<div class="btnbar">
|
<div class="btnbar">
|
||||||
<button id="expImg" class="primary">Als Bild (JPG)</button>
|
<button id="expImg" class="primary">Als Bild (JPG)</button>
|
||||||
<button id="expPdf">Drucken / PDF</button>
|
<button id="expPdf">Als PDF speichern</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -610,50 +610,91 @@ $("#resetAll").addEventListener("click",()=>{if(confirm("Wirklich alles zurücks
|
|||||||
try{localStorage.removeItem(KEY);}catch(e){}
|
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:[]};
|
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();}});
|
$("#editPrices").checked=false;$("#priceResetBar").style.display="none";initForm();renderDrinks();renderKid();compute();}});
|
||||||
$("#expPdf").addEventListener("click",()=>window.print());
|
$("#expPdf").addEventListener("click",exportPdf);
|
||||||
$("#expImg").addEventListener("click",exportImage);
|
$("#expImg").addEventListener("click",exportImage);
|
||||||
|
|
||||||
/* ---- JPG-Export (Canvas, offline) ---- */
|
/* ---- Abrechnung als Canvas rendern (offline) ---- */
|
||||||
function exportImage(){
|
function buildReceipt(){
|
||||||
const L=LAST,W=760,pad=44,dpr=2;
|
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 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);
|
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);
|
g.textBaseline="alphabetic";
|
||||||
// header
|
g.fillStyle="#eef3f6";g.fillRect(0,0,W,H);
|
||||||
g.fillStyle="#0b3a54";g.fillRect(0,0,W,92);
|
// header bar
|
||||||
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="#0b3a54";g.fillRect(0,0,W,96);
|
||||||
g.fillStyle="#fff";g.font="700 22px "+FF();g.fillText("Getränkepaket-Abrechnung",pad+46,40);
|
// sauberes Martini-Logo
|
||||||
g.fillStyle="#cfe0ea";g.font="13px "+FF();
|
const lx=pad,ly=30;
|
||||||
g.fillText(PKGNAME[L.pkg]+" · "+L.persons+"× · Tag "+L.curday+" von "+L.days+" · "+new Date().toLocaleDateString("de-DE"),pad+46,64);
|
g.fillStyle="#e2673d";
|
||||||
// rows
|
g.beginPath();g.moveTo(lx,ly);g.lineTo(lx+30,ly);g.lineTo(lx+15,ly+18);g.closePath();g.fill(); // Kelch
|
||||||
let y=132;g.textBaseline="alphabetic";
|
g.fillRect(lx+13.5,ly+18,3,15); // Stiel
|
||||||
const line=(lab,val,col)=>{g.fillStyle="#5f7480";g.font="14px "+FF();g.fillText(lab,pad,y);
|
g.fillRect(lx+5,ly+33,20,3); // Fuß
|
||||||
g.fillStyle=col||"#151b1f";g.font="700 18px "+MF();g.textAlign="right";g.fillText(val,W-pad,y);g.textAlign="left";y+=38;};
|
g.beginPath();g.arc(lx+24,ly+3,2.6,0,7);g.fillStyle="#9fd0e6";g.fill(); // Olive
|
||||||
line("Paketkosten gesamt",eur(L.pkgTotal));
|
// Titel
|
||||||
line("Angerechneter Wert (im Paket)",eur(L.consumed),"#12556f");
|
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;
|
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("Zusatzkosten (nicht im Paket)",eur(L.extra),"#c9542e");
|
||||||
line("Gratis-Bonus Kind unter 4",eur(L.kidVal),"#1f9257");
|
line("Gratis-Bonus Kind unter 4",eur(L.kidVal),"#1f9257");
|
||||||
// chart
|
// Chart
|
||||||
const cx=pad, cy=rowsTop+18, cw=W-2*pad, ch=chartH-30;
|
const cx=pad,cw=W-2*pad;
|
||||||
g.fillStyle="#5f7480";g.font="12px "+FF();g.fillText("Angerechneter Wert je Tag (Ziel "+eur(L.perDay)+")",cx,cy-8);
|
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 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);
|
const n=L.vals.length,slot=cw/n,bw=Math.min(28,slot-6);
|
||||||
// target line
|
const ty=barTop+ch-(L.perDay/scale*ch);
|
||||||
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([]);
|
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;i<n;i++){const v=L.vals[i],bh=Math.max(v/scale*ch,v>0?3:0);
|
for(let i=0;i<n;i++){const v=L.vals[i],bh=Math.max(v/scale*ch,v>0?3:0),bx=cx+i*slot+(slot-bw)/2,by=barTop+ch-bh;
|
||||||
const bx=cx+i*(cw/n)+((cw/n)-bw)/2, by=cy+ch-bh;
|
|
||||||
g.fillStyle=(i+1===L.bestDay&&v>0)?"#1f9257":(v>=L.perDay&&v>0?"#2e8f5c":"#12556f");
|
g.fillStyle=(i+1===L.bestDay&&v>0)?"#1f9257":(v>=L.perDay&&v>0?"#2e8f5c":"#12556f");
|
||||||
g.fillRect(bx,by,bw,bh);
|
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";}
|
g.fillStyle="#9fb3bf";g.font="10px "+MF();g.textAlign="center";g.fillText(i+1,bx+bw/2,barTop+ch+14);g.textAlign="left";}
|
||||||
// footer
|
// Footer
|
||||||
g.fillStyle="#5f7480";g.font="12px "+FF();
|
g.fillStyle="#5f7480";g.font="12px "+FF();
|
||||||
g.fillText("aidadrinks.heidrich-digital.de · Preisstand AIDA-Barkarte 01.11.2025 · inoffiziell",pad,H-26);
|
g.fillText("aidadrinks.heidrich-digital.de · Preisstand AIDA-Barkarte 01.11.2025 · inoffizieller Helfer",pad,footY);
|
||||||
cv.toBlob(b=>{const u=URL.createObjectURL(b),a=document.createElement("a");
|
return{cv,W,H};
|
||||||
a.href=u;a.download="Getraenkepaket-Abrechnung.jpg";a.click();setTimeout(()=>URL.revokeObjectURL(u),4000);},"image/jpeg",0.92);
|
}
|
||||||
|
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 FF(){return '-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Arial,sans-serif';}
|
||||||
function MF(){return 'ui-monospace,Menlo,Consolas,monospace';}
|
function MF(){return 'ui-monospace,Menlo,Consolas,monospace';}
|
||||||
|
|||||||
Reference in New Issue
Block a user