| Server IP : 188.114.96.3 / Your IP : 216.73.216.143 Web Server : Apache System : Linux sv058.home.net.pl 6.8.2 husak_dirty_pages_fix #1 SMP PREEMPT_DYNAMIC Tue Apr 2 22:06:51 CEST 2024 x86_64 User : ( 3194769) PHP Version : 8.5.12 Disable Function : NONE MySQL : OFF | cURL : ON | WGET : ON | Perl : ON | Python : ON | Sudo : ON | Pkexec : OFF Directory : /polana/admin/ |
Upload File : |
<?php
declare(strict_types=1);
require_once dirname(__DIR__).'/includes/tklm_word_v5.php';
require_admin_role();
header('Cache-Control: private, no-store, max-age=0');
header('X-Robots-Tag: noindex, nofollow');
header('Content-Type: text/html; charset=UTF-8');
$id=filter_input(INPUT_GET,'menu_id',FILTER_VALIDATE_INT);
$lang=strtolower((string)($_GET['lang']??'pl'));
$draft=(string)($_GET['draft']??'')==='1';
$download=(string)($_GET['download']??'')==='1';
if (!$id || !in_array($lang,['pl','en','de','es'],true)) { http_response_code(400); exit('Błędna karta lub język.'); }
try {
$st=tklm_v5_status((int)$id,$lang);
if (!$st['available']) throw new RuntimeException('Brak aktywnych pozycji lub tłumaczeń '.$lang.'.');
if (!$st['ready'] && !$draft) throw new RuntimeException('Brak '.count($st['missing']).' tłumaczeń. Wybierz wersję roboczą.');
$bundle=tklm_v7_bundle_rows((int)$id);
$allergens=['pl'=>'Alergeny','en'=>'Allergens','de'=>'Allergene','es'=>'Alérgenos'];
$title=['pl'=>'Karta restauracji','en'=>'Restaurant menu','de'=>'Speisekarte','es'=>'Carta del restaurante'];
$rows=[];
foreach ($bundle as $pack) {
$menu=$pack['menu'];
$cats=$pack['categories'];
$catRows=[];
foreach ($cats as $cat) {
if (!(int)$cat['active']) continue;
$items=[];
foreach ($cat['items'] as $item) {
if (!(int)$item['active'] || !(int)$item['available']) continue;
$texts=[];
foreach (['name','description','allergens'] as $field) {
$original=trim((string)($item[$field]??''));
$translated=trim((string)($item[$lang==='pl'?$field:$field.'_'.$lang]??''));
$texts[$field]=($lang!=='pl' && $original!=='' && $translated==='') ? '['.strtoupper($lang).' MISSING] '.$original : $translated;
}
$items[]=$texts+['price'=>tklm_v5_word_price($item)];
}
if (!$items) continue;
$src=(string)($cat['name']??'');
$translated=trim((string)($cat[$lang==='pl'?'name':'name_'.$lang]??''));
$name=($lang!=='pl' && trim($src)!=='' && $translated==='')?'['.strtoupper($lang).' MISSING] '.$src:$translated;
$catRows[]=['title'=>$name,'items'=>$items];
}
if (!$catRows) continue;
$rows[]=['section'=>tklm_v7_type_label((string)($menu['menu_type']??'food'),$lang),'menu_id'=>(int)$menu['id'],'categories'=>$catRows];
}
if (!$rows) throw new RuntimeException('Brak pozycji do eksportu.');
$payload=[
'title'=>$title[$lang], 'allergens'=>$allergens[$lang],
'menu_id'=>(int)$id, 'lang'=>$lang,'draft'=>$draft,'pages'=>$rows,
'signature'=>'Andrzej Polan'
];
$json=json_encode($payload,JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES|JSON_HEX_TAG|JSON_HEX_AMP|JSON_HEX_APOS|JSON_HEX_QUOT|JSON_THROW_ON_ERROR);
} catch(Throwable $e) {
http_response_code(409);
echo '<!doctype html><html lang="pl"><meta charset="utf-8"><title>PDF POLANA</title><body style="font-family:system-ui;max-width:700px;margin:70px auto"><h1>Nie można przygotować PDF</h1><p>'.htmlspecialchars($e->getMessage(),ENT_QUOTES|ENT_SUBSTITUTE,'UTF-8').'</p><a href="/admin/menus.php">Wróć do kart</a></body></html>';
exit;
}
function pv6h(string $v): string {return htmlspecialchars($v,ENT_QUOTES|ENT_SUBSTITUTE,'UTF-8');}
?><!doctype html>
<html lang="<?=pv6h($lang)?>">
<head>
<meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
<title>PDF <?=strtoupper($lang)?> | Moja Polana Smaków</title>
<style>
:root{color-scheme:light}*{box-sizing:border-box}
body{margin:0;color:#24231f;background:#eeeae4;font:16px/1.5 Georgia,'Times New Roman',serif}
.toolbar{position:sticky;top:0;background:#1c211b;color:#fff;padding:12px 16px;display:flex;justify-content:center;align-items:center;gap:12px;flex-wrap:wrap;font-family:system-ui}
.toolbar a,.toolbar button{background:#a1cf30;color:#111;border:0;border-radius:28px;padding:10px 17px;font:700 14px system-ui;cursor:pointer;text-decoration:none}
.toolbar .second{background:#fafafa}.toolbar span{font-size:12px;max-width:300px}
.paper{background:white;width:min(210mm,100%);min-height:297mm;margin:24px auto;padding:15mm 18mm;box-shadow:0 12px 35px #0002;text-align:center}
.logo{width:46mm;max-height:46mm;object-fit:contain;display:block;margin:0 auto 6mm}
.subtitle{font-size:12pt;letter-spacing:2px;text-transform:uppercase;color:#6e735f;margin:0 0 7mm}
.warning{border:1.5px solid #c12a20;color:#a31910;font:700 12px system-ui;padding:10px;margin-bottom:8mm}
.category{margin:0 0 8mm;break-inside:avoid-page}.page-block{page-break-after:always;break-after:page}.page-block:last-of-type{page-break-after:auto;break-after:auto}
h2{font-size:17pt;color:#234125;margin:9mm 0 5mm}
.item{display:grid;grid-template-columns:minmax(0,1fr) auto;text-align:left;gap:12px;margin:0 0 5mm;break-inside:avoid-page;align-items:start}
.item strong{font-size:12pt;font-weight:700;line-height:1.36}.item small{display:block;font-size:9.5pt;line-height:1.3;color:#666;margin-top:1mm}
.price{font-size:12pt;white-space:nowrap;font-weight:700}.footer{padding:10mm 0 0;color:#6c7168;font-size:9pt}
@page{size:A4 portrait;margin:12mm}
@media print{html,body{background:white!important}.toolbar{display:none!important}.paper{box-shadow:none;width:100%;min-height:0;margin:0;padding:0}.category,.item{break-inside:avoid-page}}
@media(max-width:650px){.paper{margin:0;padding:24px 18px;min-height:0}.toolbar{position:static}h2{font-size:20px}.item strong{font-size:15px}}
</style>
</head>
<body>
<div class="toolbar">
<button type="button" id="download">Pobierz PDF <?=strtoupper($lang)?> ↓</button>
<button type="button" class="second" onclick="window.print()">Drukuj / Zapisz jako PDF</button>
<a class="second" href="/admin/menus.php">Wróć do kart</a>
<span id="progress" role="status" aria-live="polite"></span>
</div>
<main class="paper" id="menu-preview">
<img id="logo" class="logo" src="/assets/img/logo.png" alt="Moja Polana Smaków">
<p class="subtitle"><?=pv6h($title[$lang])?></p>
<?php foreach ($rows as $page):?><section class="category page-block">
<h2><?=pv6h($page['section'])?></h2>
<?php foreach ($page['categories'] as $cat):?><h3 style="font-size:14pt;color:#5b6d43;margin:4mm 0 3mm"><?=pv6h($cat['title'])?></h3>
<?php foreach ($cat['items'] as $it):?><div class="item"><div><strong><?=pv6h($it['name'])?></strong><?php if($it['description']!==''):?><small><?=pv6h($it['description'])?></small><?php endif;?><?php if($it['allergens']!==''):?><small><?=pv6h($allergens[$lang].': '.$it['allergens'])?></small><?php endif;?></div><span class="price"><?=pv6h($it['price'])?></span></div><?php endforeach;?><?php endforeach;?></section><?php endforeach;?>
<footer class="footer"><img src="/assets/img/tklm_andrzej_polan_signature.png?v=12" alt="Podpis Andrzeja Polana" style="width:55mm;max-width:80%;height:auto;display:block;margin:0 auto"></footer>
</main>
<script type="application/json" id="menu-data"><?=$json?></script>
<script>
'use strict';
// PDF tworzony calkowicie w przegladarce: brak fontow serwera, bibliotek/CDN i plikow tymczasowych.
const DATA = JSON.parse(document.getElementById('menu-data').textContent);
const SIG = new Image();SIG.src='/assets/img/tklm_andrzej_polan_signature.png?v=12';
const BTN = document.getElementById('download');
const MSG = document.getElementById('progress');
const DW=1240, DH=1754, LEFT=106, RIGHT=DW-106, CONTENT=RIGHT-LEFT;
const latinPdfBytes = x => Uint8Array.from(x, c=>c.charCodeAt(0)&255);
function pdfFromJpegs(jpegs){
const chunks=[];let size=0,offsets=[0];
const put=v=>{const b=typeof v==='string'?latinPdfBytes(v):v;chunks.push(b);size+=b.length;};
const start=n=>{offsets[n]=size;put(n+' 0 obj\n');};
const end=()=>put('\nendobj\n');
const N=jpegs.length, kids=[];
put('%PDF-1.4\n%TKLM\n');
start(1);put('<< /Type /Catalog /Pages 2 0 R >>');end();
for(let i=0;i<N;i++)kids.push((3+3*i)+' 0 R');
start(2);put('<< /Type /Pages /Count '+N+' /Kids ['+kids.join(' ')+'] >>');end();
for(let i=0;i<N;i++){
const page=3+i*3, img=page+1, content=page+2;
const decoded=atob(jpegs[i].split(',')[1]);
start(page);put('<< /Type /Page /Parent 2 0 R /MediaBox [0 0 595.28 841.89] /Resources << /XObject << /Im0 '+img+' 0 R >> >> /Contents '+content+' 0 R >>');end();
start(img);put('<< /Type /XObject /Subtype /Image /Width '+DW+' /Height '+DH+' /ColorSpace /DeviceRGB /BitsPerComponent 8 /Filter /DCTDecode /Length '+decoded.length+' >>\nstream\n');put(decoded);put('\nendstream');end();
const draw='q 595.28 0 0 841.89 0 0 cm /Im0 Do Q\n';
start(content);put('<< /Length '+draw.length+' >>\nstream\n'+draw+'endstream');end();
}
const xref=size, total=2+3*N;
put('xref\n0 '+(total+1)+'\n0000000000 65535 f \n');
for(let n=1;n<=total;n++)put(String(offsets[n]).padStart(10,'0')+' 00000 n \n');
put('trailer\n<< /Size '+(total+1)+' /Root 1 0 R >>\nstartxref\n'+xref+'\n%%EOF');
return new Blob(chunks,{type:'application/pdf'});
}
function splitLines(ctx,text,width){
const result=[];let line='';
for(const word of String(text).split(/\s+/)){
let test=line?line+' '+word:word;
if(ctx.measureText(test).width<=width){line=test;continue;}
if(line)result.push(line);
line=word;
while(ctx.measureText(line).width>width && line.length>1){
let len=1;while(len<line.length && ctx.measureText(line.slice(0,len+1)).width<=width)len++;
result.push(line.slice(0,len));line=line.slice(len);
}
}
if(line)result.push(line);
return result.length?result:[''];
}
async function makePdf(){
BTN.disabled=true; MSG.textContent='Generuję dokument A4…';
try{
await document.fonts.ready;
const logo=document.getElementById('logo');
if(!logo.complete)await new Promise((resolve,reject)=>{logo.addEventListener('load',resolve,{once:true});logo.addEventListener('error',reject,{once:true});});
if(!logo.naturalWidth)throw Error('Nie można wczytać logo.');
if(!SIG.complete)await new Promise((resolve,reject)=>{SIG.addEventListener('load',resolve,{once:true});SIG.addEventListener('error',reject,{once:true});});
if(!SIG.naturalWidth)throw Error('Nie można wczytać graficznego podpisu.');
let pages=[],c=null,x=null,y=0,count=0;
function page(){
if(c)pages.push(c.toDataURL('image/jpeg',0.93));
c=document.createElement('canvas');c.width=DW;c.height=DH;x=c.getContext('2d');
x.fillStyle='white';x.fillRect(0,0,DW,DH);count++;y=100;
if(count===1){
const maxW=245,maxH=225,scale=Math.min(maxW/logo.naturalWidth,maxH/logo.naturalHeight);
const w=logo.naturalWidth*scale,h=logo.naturalHeight*scale;
x.drawImage(logo,(DW-w)/2,y,w,h);y+=h+38;
x.fillStyle='#68725d';x.font='26px Georgia,serif';x.textAlign='center';x.fillText(DATA.title.toUpperCase(),DW/2,y);y+=56;
// Bez czerwonego tekstu DRAFT w druku.
}else{
x.textAlign='center';x.fillStyle='#526d48';x.font='22px Georgia,serif';x.fillText('MOJA POLANA SMAKÓW',DW/2,90);
y=145;
}
x.textAlign='left';
}
function footer(){
x.fillStyle='#909487';x.textAlign='center';x.font='20px Georgia,serif';
x.fillText('Moja Polana Smaków • Kraków | '+count,DW/2,DH-207);
const sw=265,sh=sw*SIG.naturalHeight/SIG.naturalWidth;
x.drawImage(SIG,(DW-sw)/2,DH-183,sw,sh);
x.textAlign='left';
}
function nextPage(){footer();page();}
function drawText(lines,font,color,xpos,lineHeight){
x.font=font;x.fillStyle=color;x.textAlign='left';
for(const line of lines){x.fillText(line,xpos,y);y+=lineHeight;}
}
page();
let firstSection = true;
for(const pageBlock of DATA.pages){
if(!firstSection){nextPage();}
firstSection = false;
x.textAlign='center';x.fillStyle='#254b2c';x.font='bold 34px Georgia,serif';x.fillText(pageBlock.section,DW/2,y);y+=54;x.textAlign='left';
for(const cat of pageBlock.categories){
if(y+90>DH-280)nextPage();
y+=18;x.textAlign='center';x.fillStyle='#254b2c';x.font='bold 31px Georgia,serif';
x.fillText(cat.title,DW/2,y);y+=55;x.textAlign='left';
for(const it of cat.items){
x.font='bold 25px Georgia,serif';
const names=splitLines(x,it.name,CONTENT-175);
x.font='22px Georgia,serif';
const ds=it.description?splitLines(x,it.description,CONTENT-195):[];
x.font='19px Arial';
const al=it.allergens?splitLines(x,DATA.allergens+': '+it.allergens,CONTENT-195):[];
const block=names.length*36+ds.length*30+al.length*26+22;
if(y+block>DH-280)nextPage();
const startY=y;
drawText(names,'bold 25px Georgia,serif','#1c1c1c',LEFT,36);
if(it.price){x.textAlign='right';x.font='bold 25px Georgia,serif';x.fillStyle='#111';x.fillText(it.price,RIGHT,startY);x.textAlign='left';}
if(ds.length)drawText(ds,'22px Georgia,serif','#5d5d5d',LEFT,30);
if(al.length)drawText(al,'19px Arial,sans-serif','#666',LEFT,26);
y+=22;
}
y+=24;
}
}
x.fillStyle='#909487';x.textAlign='center';x.font='20px Georgia,serif';
footer();
pages.push(c.toDataURL('image/jpeg',0.93));
const blob=pdfFromJpegs(pages);
const u=URL.createObjectURL(blob),a=document.createElement('a');
a.href=u;a.download='Polana_Menu_'+DATA.menu_id+'_'+DATA.lang.toUpperCase()+(DATA.draft?'_ROBOCZE':'')+'.pdf';
document.body.append(a);a.click();a.remove();
setTimeout(()=>URL.revokeObjectURL(u),15000);
MSG.textContent='Gotowe: '+pages.length+' str. PDF';
}catch(e){MSG.textContent='Błąd PDF: '+e.message;console.error(e);}
finally{BTN.disabled=false;}
}
BTN.addEventListener('click',makePdf);
<?php if($download):?>window.addEventListener('load',()=>makePdf());<?php endif;?>
</script>
</body></html>