| 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);
/** POLANA V16.5: unified menu workspace. No schema changes, no deletes. */
require_once dirname(__DIR__).'/includes/functions.php';
require_login();
$who=admin_user();
$role=(string)($who['role']??'');
if (!in_array($role,['admin','editor'],true)) {http_response_code(403);exit('Brak uprawnień do edycji menu.');}
header('Cache-Control: private, no-store, max-age=0');
header('X-Robots-Tag: noindex, nofollow');
function pm165_html($v):string{return htmlspecialchars((string)$v,ENT_QUOTES|ENT_SUBSTITUTE,'UTF-8');}
function pm165_len(string $s):int{return function_exists('mb_strlen')?mb_strlen($s,'UTF-8'):strlen($s);}
function pm165_price($raw):?string{
$s=str_replace(',','.',trim((string)$raw));
if($s==='')return null;
if(!preg_match('/^\\d{1,6}(?:\\.\\d{1,2})?$/D',$s)|| (float)$s>999999)throw new RuntimeException('Cena musi być liczbą od 0 do 999999 (maks. 2 miejsca po przecinku).');
return number_format((float)$s,2,'.','');
}
function pm165_name($raw,int $max,string $title):string{
$s=trim((string)$raw);
if($s===''||pm165_len($s)>$max)throw new RuntimeException($title.': wymagane 1–'.$max.' znaków.');
return $s;
}
function pm165_short($raw,int $max,string $title):string{
$s=trim((string)$raw);
if(pm165_len($s)>$max)throw new RuntimeException($title.': maksymalnie '.$max.' znaków.');
return $s;
}
function pm165_menu(int $id):array{
$q=db()->prepare('SELECT * FROM menus WHERE id=? LIMIT 1');$q->execute([$id]);
$r=$q->fetch(PDO::FETCH_ASSOC);if(!$r)throw new RuntimeException('Wybrana karta nie istnieje.');return $r;
}
function pm165_cat(int $id,int $menuId):array{
$q=db()->prepare('SELECT * FROM menu_categories WHERE id=? AND menu_id=? LIMIT 1');$q->execute([$id,$menuId]);
$r=$q->fetch(PDO::FETCH_ASSOC);if(!$r)throw new RuntimeException('Kategoria nie należy do tej karty.');return $r;
}
function pm165_item(int $id,int $menuId):array{
$q=db()->prepare('SELECT i.* FROM menu_items i JOIN menu_categories c ON c.id=i.category_id WHERE i.id=? AND c.menu_id=? LIMIT 1');
$q->execute([$id,$menuId]);$r=$q->fetch(PDO::FETCH_ASSOC);
if(!$r)throw new RuntimeException('Pozycja nie należy do tej karty.');return $r;
}
function pm165_enqueue(array $changes,bool $published):bool{
if(!$published||!$changes||!is_file(dirname(__DIR__).'/includes/tklm_v9.php'))return false;
try{
require_once dirname(__DIR__).'/includes/tklm_v9.php';
v9_init();
foreach($changes as $c){foreach(['en','de','es'] as $lang){v9_enqueue($c[0],(int)$c[1],$c[2],$lang,(string)$c[3]);}}
return true;
}catch(Throwable $e){error_log('POLANA V16.5 queued translation failure: '.$e->getMessage());return false;}
}
function pm165_next_order(string $table,string $parent,int $id):int{
if(!in_array($table,['menu_categories','menu_items'],true)||!in_array($parent,['menu_id','category_id'],true))throw new RuntimeException('Błędny rejestr');
$q=db()->prepare('SELECT COALESCE(MAX(sort_order),0)+10 FROM '.$table.' WHERE '.$parent.'=?');
$q->execute([$id]);return (int)$q->fetchColumn();
}
function pm165_return(int $id,string $part='',string $type='food',bool $changed=false):void{
/* PIN77_AUTO_MENU_V18: notify only after successful edits of published cards. */
if($changed){
try {
$row=pm165_menu($id);
if(($row['status']??'')==='published'){
require_once dirname(__DIR__).'/includes/polana_indexnow.php';
pin77_after_menu_change();
}
}catch(Throwable $e){error_log('POLANA V18 menu IndexNow: '.$e->getMessage());}
}
$query=http_build_query(['type'=>$type,'menu_id'=>$id]);
redirect('/admin/polana-menu-centrum.php?'.$query.($part?'#'.$part:''));
}
$type=(string)($_GET['type']??$_POST['type']??'food');
if(!in_array($type,['food','soft','wine'],true))$type='food';
$menuId=(int)($_GET['menu_id']??$_POST['menu_id']??0);
if(($_SERVER['REQUEST_METHOD']??'GET')==='POST'){
verify_csrf();
try{
$action=(string)($_POST['action']??'');
$menu=pm165_menu($menuId);$type=(string)$menu['menu_type'];
$published=$menu['status']==='published';
$changes=[];
if($action==='save_item'){
$id=(int)($_POST['id']??0);$old=pm165_item($id,$menuId);
$name=pm165_name($_POST['name']??'',255,'Nazwa pozycji');
$desc=trim((string)($_POST['description']??''));if(pm165_len($desc)>12000)throw new RuntimeException('Opis za długi.');
$all=pm165_short($_POST['allergens']??'',500,'Alergeny');
$weight=pm165_short($_POST['weight']??'',100,'Gramatura');
$priceText=pm165_short($_POST['price_text']??'',100,'Cena tekstowa');
$price=pm165_price($_POST['price']??'');
$sort=max(-100000,min(100000,(int)($_POST['sort_order']??$old['sort_order'])));
$q=db()->prepare('UPDATE menu_items SET name=?,description=?,allergens=?,weight=?,price=?,price_text=?,sort_order=?,active=?,available=?,featured=? WHERE id=?');
$q->execute([$name,$desc,$all,$weight,$price,$priceText,$sort,isset($_POST['active'])?1:0,isset($_POST['available'])?1:0,isset($_POST['featured'])?1:0,$id]);
foreach(['name'=>$name,'description'=>$desc,'allergens'=>$all] as $field=>$value){
if(trim((string)($old[$field]??''))!==$value && $value!=='')$changes[]=['menu_items',$id,$field,$value];
}
admin_log('polana_menu_quick_edit','Zmodyfikowano pozycję #'.$id.' / menu #'.$menuId);
$queued=pm165_enqueue($changes,$published);
flash('success','Zapisano danie i cenę.'.($queued?' Zmienione teksty dodano do tłumaczenia.':(($changes && $published)?' Sprawdź kolejkę tłumaczeń w Centrum Tłumaczeń.':'')));
pm165_return($menuId,'item-'.$id,$type,true);
}elseif($action==='save_category'){
$id=(int)($_POST['id']??0);$old=pm165_cat($id,$menuId);
$name=pm165_name($_POST['name']??'',150,'Nazwa kategorii');
$sort=max(-100000,min(100000,(int)($_POST['sort_order']??$old['sort_order'])));
$q=db()->prepare('UPDATE menu_categories SET name=?,sort_order=?,active=? WHERE id=?');
$q->execute([$name,$sort,isset($_POST['active'])?1:0,$id]);
if($name!==trim((string)$old['name']))$changes[]=['menu_categories',$id,'name',$name];
$queued=pm165_enqueue($changes,$published);
admin_log('polana_menu_category_edit','Kategoria #'.$id.' / menu #'.$menuId);
flash('success','Zapisano kategorię.'.($queued?' Zlecono tłumaczenia.':(($changes && $published)?' Sprawdź kolejkę tłumaczeń w Centrum Tłumaczeń.':'')));
pm165_return($menuId,'cat-'.$id,$type,true);
}elseif($action==='add_category'){
$name=pm165_name($_POST['name']??'',150,'Nazwa kategorii');
$sort=pm165_next_order('menu_categories','menu_id',$menuId);
$q=db()->prepare('INSERT INTO menu_categories(menu_id,name,sort_order,active) VALUES(?,?,?,1)');
$q->execute([$menuId,$name,$sort]);$id=(int)db()->lastInsertId();
pm165_enqueue([['menu_categories',$id,'name',$name]],$published);
admin_log('polana_menu_add_category','Kategoria #'.$id.' / menu #'.$menuId);
flash('success','Dodano kategorię.');pm165_return($menuId,'cat-'.$id,$type,true);
}elseif($action==='add_item'){
$catId=(int)($_POST['category_id']??0);pm165_cat($catId,$menuId);
$name=pm165_name($_POST['name']??'',255,'Nazwa dania');
$desc=trim((string)($_POST['description']??''));if(pm165_len($desc)>12000)throw new RuntimeException('Opis za długi.');
$price=pm165_price($_POST['price']??'');$sort=pm165_next_order('menu_items','category_id',$catId);
$q=db()->prepare('INSERT INTO menu_items(category_id,name,description,price,sort_order,active,available) VALUES(?,?,?,?,?,1,1)');
$q->execute([$catId,$name,$desc,$price,$sort]);$id=(int)db()->lastInsertId();
$changes=[['menu_items',$id,'name',$name]];
if($desc!=='')$changes[]=['menu_items',$id,'description',$desc];
pm165_enqueue($changes,$published);
admin_log('polana_menu_add_item','Danie #'.$id.' / menu #'.$menuId);
flash('success','Dodano pozycję.');pm165_return($menuId,'item-'.$id,$type,true);
}elseif($action==='bulk_prices'){
$prices=$_POST['prices']??[];$checks=$_POST['available']??[];
if(!is_array($prices)||!is_array($checks)||count($prices)>350)throw new RuntimeException('Nieprawidłowa lista cen.');
$look=db()->prepare('SELECT i.id,i.price,i.available FROM menu_items i JOIN menu_categories c ON c.id=i.category_id WHERE i.id=? AND c.menu_id=?');
$upd=db()->prepare('UPDATE menu_items SET price=?,available=? WHERE id=?');
$count=0;
db()->beginTransaction();
try{
foreach($prices as $rawId=>$rawPrice){
if(!is_scalar($rawId)||!ctype_digit((string)$rawId))throw new RuntimeException('Nieprawidłowy identyfikator');
$id=(int)$rawId;$look->execute([$id,$menuId]);$old=$look->fetch(PDO::FETCH_ASSOC);
if(!$old)throw new RuntimeException('Danie #'.$id.' nie należy do tej karty.');
$price=pm165_price($rawPrice);$available=isset($checks[$id])?1:0;
if(($old['price']===null?'':number_format((float)$old['price'],2,'.',''))!==($price??'')||(int)$old['available']!==$available){
$upd->execute([$price,$available,$id]);$count++;
}
}
db()->commit();
}catch(Throwable $e){db()->rollBack();throw $e;}
admin_log('polana_menu_bulk_prices','Menu #'.$menuId.', poprawiono '.$count.' pozycji');
flash('success','Zapisano ceny i dostępność: '.$count.' zmian. Tłumaczenia nie wymagały aktualizacji.');
pm165_return($menuId,'ceny',$type,true);
}else throw new RuntimeException('Nieznana akcja.');
}catch(Throwable $e){
flash('error','Nie zapisano zmian: '.$e->getMessage());
pm165_return($menuId,'dania',$type);
}
}
$allMenus=[];$menusByType=['food'=>[],'soft'=>[],'wine'=>[]];
try{
$allMenus=db()->query("SELECT id,name,menu_type,status,valid_from,valid_to,updated_at FROM menus ORDER BY id DESC")->fetchAll(PDO::FETCH_ASSOC);
foreach($allMenus as $row){$mt=(string)$row['menu_type'];if(isset($menusByType[$mt]))$menusByType[$mt][]=$row;}
}catch(Throwable $e){http_response_code(500);exit('Nie można odczytać menu z MariaDB.');}
$today=date('Y-m-d');$best=[];
foreach($menusByType as $kind=>$records){
foreach($records as $r){
if($r['status']==='published' && (empty($r['valid_from'])||$r['valid_from']<=$today) && (empty($r['valid_to'])||$r['valid_to']>=$today)){$best[$kind]=$r;break;}
}
if(!isset($best[$kind]))foreach($records as $r){if($r['status']==='published'){$best[$kind]=$r;break;}}
if(!isset($best[$kind]) && $records)$best[$kind]=$records[0];
}
$menuRow=null;
if($menuId>0){foreach($allMenus as $r){if((int)$r['id']===$menuId){$menuRow=$r;$type=(string)$r['menu_type'];break;}}}
if(!$menuRow && isset($best[$type])){$menuRow=$best[$type];$menuId=(int)$menuRow['id'];}
$cats=[];$itemsByCat=[];$allItems=[];$translation=['en'=>['total'=>0,'missing'=>0],'de'=>['total'=>0,'missing'=>0],'es'=>['total'=>0,'missing'=>0]];
$queue=['pending'=>0,'review'=>0,'approved'=>0,'failed'=>0];$dataError='';
if($menuRow){
try{
$q=db()->prepare('SELECT * FROM menu_categories WHERE menu_id=? ORDER BY sort_order,id');$q->execute([$menuId]);$cats=$q->fetchAll(PDO::FETCH_ASSOC);
$itemsQ=db()->prepare('SELECT * FROM menu_items WHERE category_id=? ORDER BY sort_order,id');
foreach($cats as $cat){
$itemsQ->execute([(int)$cat['id']]);$items=$itemsQ->fetchAll(PDO::FETCH_ASSOC);$itemsByCat[(int)$cat['id']]=$items;
foreach($items as $item)$allItems[]=$item+['category_name'=>$cat['name']];
if(!(int)$cat['active'])continue;
foreach(['en','de','es'] as $lang){$translation[$lang]['total']++;if(trim((string)($cat['name_'.$lang]??''))==='')$translation[$lang]['missing']++;}
foreach($items as $item){
if(!(int)$item['active'] || !(int)$item['available'])continue;
foreach(['name','description','allergens'] as $field){if(trim((string)($item[$field]??''))==='')continue;
foreach(['en','de','es'] as $lang){$translation[$lang]['total']++;if(trim((string)($item[$field.'_'.$lang]??''))==='')$translation[$lang]['missing']++;}
}
}
}
try{
$q=db()->prepare("SELECT j.status,j.attempts,COUNT(*) n FROM tklm_v9_jobs j LEFT JOIN menu_categories c ON j.entity='menu_categories' AND c.id=j.entity_id LEFT JOIN menu_items i ON j.entity='menu_items' AND i.id=j.entity_id WHERE ((j.entity='menu_categories' AND c.menu_id=? AND c.active=1) OR (j.entity='menu_items' AND i.category_id IN(SELECT id FROM menu_categories WHERE menu_id=? AND active=1) AND i.active=1 AND i.available=1)) GROUP BY j.status,j.attempts");
$q->execute([$menuId,$menuId]);
foreach($q->fetchAll(PDO::FETCH_ASSOC) as $j){$st=(string)$j['status'];$n=(int)$j['n'];if($st==='pending' && (int)$j['attempts']>=5)$queue['failed']+=$n;elseif(isset($queue[$st]))$queue[$st]+=$n;}
}catch(Throwable $e){/* Translation queue may be absent, editing remains operational. */}
}catch(Throwable $e){$dataError='Błąd odczytu pozycji: '.$e->getMessage();}
}
$labels=['food'=>'Dania','soft'=>'Napoje','wine'=>'Wina'];
render_admin_header('Menu — szybka edycja | Polana');
?>
<style>
.pmm{--forest:#284c32;--forest2:#17371e;--muted:#647468;--line:#dde7dc;--soft:#f4f8f3;--orange:#c77837;max-width:1340px;margin:0 auto 76px;padding:14px;color:#1e3023;font-family:inherit}
.pmm *{box-sizing:border-box}.pmm h1{font-size:clamp(26px,4vw,38px);margin:6px 0}.pmm h2{font-size:21px;margin:0 0 10px}.pmm h3{font-size:17px;margin:0}.pmm .muted{color:var(--muted);font-size:14px;line-height:1.55}.pmm a{color:var(--forest2)}
.pmm .top{display:flex;justify-content:space-between;gap:12px;flex-wrap:wrap;align-items:center;margin-bottom:18px}
.pmm .button,.pmm button{border:0;background:var(--forest);border-radius:11px;padding:10px 16px;color:white;font:inherit;font-weight:650;cursor:pointer;min-height:43px;text-decoration:none;display:inline-flex;align-items:center;justify-content:center;gap:5px}
.pmm .button.alt,.pmm button.alt{background:#eef4eb;color:#203d29;border:1px solid #cddccb}.pmm .button.orange,.pmm button.orange{background:var(--orange);color:white}.pmm .button:focus-visible,.pmm button:focus-visible,.pmm input:focus-visible,.pmm select:focus-visible,.pmm textarea:focus-visible{outline:3px solid #a1c7a7;outline-offset:2px}
.pmm .types{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:12px;margin:16px 0}.pmm .type{background:#fff;color:#1c3323;border:2px solid var(--line);border-radius:16px;padding:15px;text-decoration:none;min-height:105px;display:flex;flex-direction:column;gap:6px}.pmm .type.active{border-color:#598862;background:#edf7ec}.pmm .type strong{font-size:19px}.pmm .type span{font-size:12px;color:var(--muted)}
.pmm .panel{background:#fff;border:1px solid var(--line);border-radius:17px;padding:17px;margin:16px 0}.pmm .toolbar{display:flex;gap:8px;flex-wrap:wrap;align-items:center}.pmm .tabs{position:sticky;top:0;z-index:5;background:#f7faf7;padding:10px;border:1px solid var(--line);border-radius:12px;display:flex;gap:6px;flex-wrap:wrap}.pmm .tabs a{padding:10px 12px;border-radius:10px;background:#fff;border:1px solid var(--line);text-decoration:none;font-weight:650}
.pmm label{display:block;font-size:13px;font-weight:650}.pmm input[type=text],.pmm input[type=search],.pmm input[type=number],.pmm select,.pmm textarea{background:#fff;color:#1c2920;border:1px solid #bfcfc0;border-radius:10px;padding:10px;width:100%;min-width:0;font:inherit;min-height:43px}.pmm textarea{min-height:68px;resize:vertical}.pmm .fields{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px}.pmm label span{display:block;margin-bottom:6px}.pmm .check{display:inline-flex;gap:7px;align-items:center;padding:6px 0}.pmm .check input{width:18px;height:18px}
.pmm .category{border:1px solid #d6e3d7;border-radius:14px;margin:14px 0;overflow:hidden;background:#fff}.pmm .cat-title{padding:13px 15px;background:var(--soft);display:flex;gap:10px;justify-content:space-between;flex-wrap:wrap;align-items:center}.pmm .cat-title strong{font-size:18px}.pmm .item{border-top:1px solid #e4ece4;padding:13px 15px}.pmm .item-head{display:grid;grid-template-columns:1fr 125px auto;align-items:center;gap:12px}.pmm .item-head .item-name{min-width:0}.pmm .item-head .price{max-width:125px}.pmm .btnbox{display:flex;gap:7px;align-items:center;flex-wrap:wrap}.pmm .details{margin-top:9px}.pmm summary{cursor:pointer;color:var(--forest);font-weight:700;padding:8px 0}.pmm .small{font-size:12px}.pmm .chips{display:flex;flex-wrap:wrap;gap:8px}.pmm .chip{font-size:12px;padding:6px 10px;border:1px solid #d4e2d4;border-radius:99px;background:#f3f7f2}.pmm .ok{color:#246632}.pmm .warning{color:#9d5f22}.pmm .grid4{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:10px}
.pmm .table{width:100%;border-collapse:collapse}.pmm .table td,.pmm .table th{padding:9px 7px;border-bottom:1px solid #e3ebe2;text-align:left}.pmm .table input[type=text]{max-width:135px}.pmm .table input[type=checkbox]{width:19px;height:19px}.pmm .right{text-align:right}.pmm .subnav{margin:8px 0 4px}.pmm .note{background:#f6f7e9;border-left:4px solid #d39c4a;padding:13px;border-radius:7px}.pmm .hidden{display:none!important}.pmm .sr{position:absolute;clip:rect(0,0,0,0);width:1px;height:1px;overflow:hidden}
@media(max-width:850px){.pmm .grid4{grid-template-columns:repeat(2,1fr)}.pmm .item-head{grid-template-columns:1fr 90px}.pmm .btnbox{grid-column:1 / -1}.pmm .fields{grid-template-columns:1fr}.pmm .tabs{position:static}}
@media(max-width:520px){.pmm{padding:8px}.pmm .panel{padding:12px}.pmm .types{gap:7px}.pmm .type{padding:10px;min-height:90px}.pmm .type strong{font-size:16px}.pmm .types .type span{font-size:11px}.pmm .toolbar .button{flex:1}.pmm .item{padding:12px 9px}.pmm .item-head{grid-template-columns:1fr 105px}.pmm .item-head .btnbox{grid-column:1 / -1}.pmm .table{font-size:13px}}
.pmm .pmm-action-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:10px}
.pmm .pmm-shortcut{display:flex;flex-direction:column;gap:7px;min-height:76px;padding:14px;text-decoration:none;background:#f4f8f2;border:1px solid #d2e1d1;border-radius:12px;color:#22452b}
.pmm .pmm-shortcut:hover,.pmm .pmm-shortcut:focus-visible{outline:2px solid #60916b;background:#eaf5e8}
.pmm .pmm-shortcut b{font-size:14px}.pmm .pmm-shortcut small{color:#617468;font-size:12px;line-height:1.4}
@media(max-width:1060px){.pmm .pmm-action-grid{grid-template-columns:repeat(3,minmax(0,1fr))}}
@media(max-width:700px){.pmm .pmm-action-grid{grid-template-columns:repeat(2,minmax(0,1fr))}}
@media(max-width:410px){.pmm .pmm-action-grid{grid-template-columns:1fr}}
</style>
<main class="pmm">
<div class="top"><div><h1>Menu restauracji</h1><p class="muted" style="margin:3px 0">Jedno miejsce: dania, ceny, wersje językowe, druk i publikacja.</p></div>
<div class="toolbar"><a class="button alt" href="/admin/polana-centrum-tresci.php">← Centrum treści</a><a class="button alt" href="/admin/menus.php">Wszystkie wersje menu</a></div></div>
<nav class="types" aria-label="Rodzaj menu">
<?php foreach($labels as $kind=>$label):$m=$best[$kind]??null;?><a class="type <?=$kind===$type?'active':''?>" href="?<?=pm165_html(http_build_query(['type'=>$kind]))?>"><strong><?=pm165_html(['food'=>'🍽️','soft'=>'🥤','wine'=>'🍷'][$kind])?> <?=pm165_html($label)?></strong><span><?=$m?pm165_html($m['name']).' · '.pm165_html($m['status']):'Brak karty'?></span></a><?php endforeach;?>
</nav>
<?php if(!$menuRow):?><section class="panel"><h2>Nie ma jeszcze karty <?=pm165_html($labels[$type])?></h2><p>Utwórz ją w dotychczasowym panelu kart. Wcześniejszych danych nie usuwamy.</p><a class="button" href="/admin/menus.php">Dodaj menu</a></section><?php else:?>
<section class="panel" id="karta"><div class="top"><div><h2><?=pm165_html($menuRow['name'])?> <span class="small" style="color:#678">#<?=$menuId?></span></h2><p class="muted" style="margin:0">Status: <b><?=pm165_html($menuRow['status'])?></b> · Ważne: <?=pm165_html($menuRow['valid_from']?:'bez daty')?> – <?=pm165_html($menuRow['valid_to']?:'bez daty')?></p></div><a class="button alt" href="/admin/menu.php?id=<?=$menuId?>">Daty / publikacja / ustawienia</a></div>
<form method="get" class="toolbar"><input type="hidden" name="type" value="<?=pm165_html($type)?>"><label for="pmmMenu">Wersja karty: </label><select id="pmmMenu" name="menu_id" onchange="this.form.submit()" style="max-width:550px">
<?php foreach($menusByType[$type] as $r):?><option value="<?=(int)$r['id']?>" <?=(int)$r['id']===$menuId?'selected':''?>>#<?=(int)$r['id']?> — <?=pm165_html($r['name'])?> (<?=pm165_html($r['status'])?>)</option><?php endforeach;?></select><noscript><button>Otwórz</button></noscript></form></section>
<section class="panel pmm-all-actions" id="funkcje">
<h2>Wszystkie przyciski menu — od razu dostępne</h2>
<p class="muted">Nie trzeba otwierać „Więcej narzędzi”. Wybierz rodzaj karty Dania / Napoje / Wina u góry, a potem potrzebną funkcję.</p>
<div class="pmm-action-grid">
<a href="#dania" class="pmm-shortcut"><b>✏️ Edytuj dania i opisy</b><small>Wszystkie pozycje oraz kategorie</small></a>
<a href="#ceny" class="pmm-shortcut"><b>💰 Ceny zbiorczo</b><small>Zmiany cen i dostępności</small></a>
<a href="#nowa-kategoria" class="pmm-shortcut"><b>➕ Nowa kategoria</b><small>Dodawanie bez przechodzenia do innych paneli</small></a>
<a href="/admin/menus.php" class="pmm-shortcut"><b>📚 Wszystkie karty</b><small>Nowa karta, archiwum i historia</small></a>
<a href="/admin/menu.php?id=<?=$menuId?>" class="pmm-shortcut"><b>📅 Publikacja i daty</b><small>Ważność, stan i ustawienia karty</small></a>
<?php if($role==='admin'):?><a href="/admin/polana-v16-tlumaczenia.php?area=menu&menu_id=<?=$menuId?>&lang=all" class="pmm-shortcut"><b>🌍 Tłumacz EN / DE / ES</b><small>Jedna kolejka i zatwierdzanie zbiorcze</small></a><?php endif;?>
<a href="#eksport" class="pmm-shortcut"><b>📄 Word / PDF</b><small>Wszystkie cztery języki</small></a>
<a href="/admin/import.php" class="pmm-shortcut"><b>📥 Import Word</b><small>Importowanie menu z dokumentu</small></a>
<a href="/admin/media.php" class="pmm-shortcut"><b>📸 Zdjęcia dań</b><small>Dodawanie i edycja fotografii</small></a>
<a href="/admin/visuals.php" class="pmm-shortcut"><b>🎨 Grafiki</b><small>Banery i materiały wizualne</small></a>
<a href="/admin/qr.php" class="pmm-shortcut"><b>▦ Kody QR</b><small>Menu na stolikach</small></a>
<a href="/menu?kind=<?=pm165_html($type)?>" target="_blank" rel="noopener" class="pmm-shortcut"><b>👁️ Podgląd karty</b><small>Jak widzi ją gość</small></a>
<?php if($role==='admin'):?><a href="/admin/polana-v164-seo.php" class="pmm-shortcut"><b>🔎 SEO i Google</b><small>Mapa strony, indeksacja i podstrony</small></a><?php endif;?>
<a href="/admin/blog.php" class="pmm-shortcut"><b>📰 Blog</b><small>Artykuły i harmonogram</small></a>
<a href="/admin/modules.php" class="pmm-shortcut"><b>🏠 Podstrony</b><small>Sekcje na stronie</small></a>
<a href="/admin/socials.php" class="pmm-shortcut"><b>📣 Social media</b><small>Odnośniki społecznościowe</small></a>
<a href="/admin/polana-centrum-tresci.php" class="pmm-shortcut"><b>🏡 Centrum treści</b><small>Pulpit wszystkich funkcji</small></a>
<?php if($role==='admin'):?><a href="/admin/polana-v166-sitemap.php" class="pmm-shortcut"><b>🗺️ Diagnostyka XML</b><small>Sprawdź /sitemap.xml i napraw routing</small></a><?php endif;?>
</div>
<?php if($role==='admin'):?>
<h3 style="margin:18px 0 8px">Pobierz menu od razu — Word / PDF</h3>
<div class="toolbar pmm-export-actions">
<?php foreach(['pl'=>'PL','en'=>'EN','de'=>'DE','es'=>'ES'] as $lg=>$label):$draft=($lg!=='pl'&&($translation[$lg]['missing']??0)>0); ?>
<a class="button alt" href="/admin/tklm-word-export-v5.php?menu_id=<?=$menuId?>&lang=<?=$lg?><?=$draft?'&draft=1':''?>">Word <?=$label?></a>
<a class="button alt" target="_blank" rel="noopener" href="/admin/tklm-pdf-export-v6.php?menu_id=<?=$menuId?>&lang=<?=$lg?><?=$draft?'&draft=1':''?>">PDF <?=$label?></a>
<?php endforeach;?>
</div>
<?php endif;?>
<h3 style="margin:18px 0 8px">Publiczne karty w czterech językach</h3>
<div class="toolbar">
<a class="button alt" target="_blank" rel="noopener" href="/menu?kind=<?=pm165_html($type)?>">PL</a>
<?php foreach(['en','de','es'] as $lg):?><a class="button alt" target="_blank" rel="noopener" href="/<?=$lg?>/menu?kind=<?=pm165_html($type)?>"><?=strtoupper($lg)?></a><?php endforeach;?>
</div>
</section>
<nav class="tabs" aria-label="Nawigacja po formularzach"><a href="#dania">Edytuj pozycje</a><a href="#ceny">Ceny</a><a href="#jezyki">Języki</a><a href="#eksport">Word / PDF</a></nav>
<?php if($dataError):?><p class="note"><?=pm165_html($dataError)?></p><?php endif;?>
<section id="dania" class="panel">
<div class="top"><div><h2>Dania i kategorie</h2><p class="muted">Wpisz nazwę lub cenę, kliknij „Zapisz”. Rozwinięcie pozycji pokazuje opis i alergeny.</p></div><label style="min-width:210px;flex:0 1 310px"><span>Szukaj w tej karcie</span><input id="pmm-search" type="search" placeholder="np. pierogi, tatar, rosół" autocomplete="off"></label></div>
<?php foreach($cats as $cat):$cid=(int)$cat['id'];$items=$itemsByCat[$cid]??[];?><section class="category" id="cat-<?=$cid?>">
<div class="cat-title"><strong><?=pm165_html($cat['name'])?> <small>(<?=count($items)?>)</small></strong><span class="muted"><?=empty($cat['active'])?'Ukryta':'Widoczna'?></span></div>
<details open style="padding:0 15px"><summary>Edytuj kategorię i kolejność</summary><form method="post" class="fields" style="padding:9px 0 14px">
<?=csrf_field()?><input type="hidden" name="action" value="save_category"><input type="hidden" name="menu_id" value="<?=$menuId?>"><input type="hidden" name="type" value="<?=pm165_html($type)?>"><input type="hidden" name="id" value="<?=$cid?>">
<label><span>Nazwa po polsku</span><input type="text" name="name" maxlength="150" required value="<?=pm165_html($cat['name'])?>"></label><label><span>Kolejność</span><input type="number" name="sort_order" value="<?=(int)$cat['sort_order']?>"></label>
<label class="check"><input type="checkbox" name="active" <?=!empty($cat['active'])?'checked':''?>> Widoczna</label><button>Zapisz kategorię</button></form></details>
<?php foreach($items as $item):$iid=(int)$item['id'];?><article class="item" id="item-<?=$iid?>" data-search="<?=pm165_html((string)$cat['name'].' '.(string)$item['name'].' '.(string)($item['description']??''))?>">
<form method="post" class="pmm-item-form">
<?=csrf_field()?><input type="hidden" name="action" value="save_item"><input type="hidden" name="menu_id" value="<?=$menuId?>"><input type="hidden" name="type" value="<?=pm165_html($type)?>"><input type="hidden" name="id" value="<?=$iid?>">
<div class="item-head"><label class="item-name"><span>Nazwa po polsku</span><input type="text" name="name" maxlength="255" required value="<?=pm165_html($item['name'])?>"></label><label class="price"><span>Cena zł</span><input type="text" inputmode="decimal" name="price" value="<?=pm165_html($item['price']??'')?>"></label>
<div class="btnbox"><label class="check"><input type="checkbox" name="available" <?=!empty($item['available'])?'checked':''?>> Dostępne</label><button type="submit">Zapisz</button><a class="small" href="/admin/item.php?id=<?=$iid?>" title="Pełny edytor, zdjęcie">Zdjęcie ↗</a></div></div>
<details class="details" open><summary>Opis, alergeny i dodatkowe ustawienia</summary><div class="fields" style="padding:10px 0">
<label><span>Opis po polsku</span><textarea name="description" maxlength="12000"><?=pm165_html($item['description']??'')?></textarea></label>
<label><span>Alergeny</span><textarea name="allergens" maxlength="500"><?=pm165_html($item['allergens']??'')?></textarea></label>
<label><span>Cena opisowa</span><input type="text" maxlength="100" name="price_text" value="<?=pm165_html($item['price_text']??'')?>"></label>
<label><span>Gramatura</span><input type="text" maxlength="100" name="weight" value="<?=pm165_html($item['weight']??'')?>"></label>
<label><span>Kolejność</span><input type="number" name="sort_order" value="<?=(int)$item['sort_order']?>"></label>
<div class="toolbar"><label class="check"><input type="checkbox" name="active" <?=!empty($item['active'])?'checked':''?>> Aktywne</label><label class="check"><input type="checkbox" name="featured" <?=!empty($item['featured'])?'checked':''?>> Wyróżnione</label></div>
</div><p class="muted small">Wersje EN/DE/ES zmienionych tekstów trafiają do kolejki; publikacja wymaga zatwierdzenia.</p></details>
</form></article><?php endforeach;?>
<details open style="padding:12px 15px;border-top:1px solid #e4ece4"><summary>+ Dodaj pozycję do: <?=pm165_html($cat['name'])?></summary>
<form method="post" class="fields" style="padding:12px 0"><?=csrf_field()?><input type="hidden" name="action" value="add_item"><input type="hidden" name="menu_id" value="<?=$menuId?>"><input type="hidden" name="type" value="<?=pm165_html($type)?>"><input type="hidden" name="category_id" value="<?=$cid?>">
<label><span>Nazwa PL</span><input type="text" required maxlength="255" name="name"></label><label><span>Cena PLN</span><input type="text" name="price" inputmode="decimal"></label><label><span>Opis</span><textarea name="description"></textarea></label><div style="align-self:end"><button>+ Dodaj danie</button></div></form></details>
</section><?php endforeach;?>
<details id="nowa-kategoria" open class="panel" style="margin-bottom:0"><summary>+ Nowa kategoria w tej karcie</summary><form method="post" class="toolbar" style="margin-top:12px"><?=csrf_field()?><input type="hidden" name="action" value="add_category"><input type="hidden" name="menu_id" value="<?=$menuId?>"><input type="hidden" name="type" value="<?=pm165_html($type)?>"><label style="flex:1"><span>Nazwa kategorii PL</span><input type="text" name="name" required maxlength="150" placeholder="np. Przystawki"></label><button>Dodaj kategorię</button></form></details>
</section>
<section class="panel" id="ceny"><div class="top"><div><h2>Ceny i dostępność — masowa edycja</h2><p class="muted">Zmień wiele cen i zaznacz dostępność. Jeden zapis, bez odświeżania każdej pozycji osobno.</p></div></div>
<?php if($allItems):?><form method="post"><?=csrf_field()?><input type="hidden" name="action" value="bulk_prices"><input type="hidden" name="menu_id" value="<?=$menuId?>"><input type="hidden" name="type" value="<?=pm165_html($type)?>">
<div style="overflow-x:auto"><table class="table"><thead><tr><th>Pozycja</th><th>Cena zł</th><th>Dostępne</th></tr></thead><tbody>
<?php foreach($allItems as $row):$iid=(int)$row['id'];?><tr><td><strong><?=pm165_html($row['name'])?></strong><div class="muted small"><?=pm165_html($row['category_name'])?></div></td><td><label class="sr" for="pmm-price-<?=$iid?>">Cena <?=pm165_html($row['name'])?></label><input id="pmm-price-<?=$iid?>" type="text" inputmode="decimal" name="prices[<?=$iid?>]" value="<?=pm165_html($row['price']??'')?>"></td><td><input aria-label="Dostępność: <?=pm165_html($row['name'])?>" type="checkbox" name="available[<?=$iid?>]" value="1" <?=!empty($row['available'])?'checked':''?>></td></tr><?php endforeach;?></tbody></table></div>
<div class="toolbar" style="margin-top:15px"><button class="orange">Zapisz wszystkie ceny i dostępność</button><span class="muted">Ceny nie wymagają ponownego tłumaczenia.</span></div></form><?php else:?><p>Brak pozycji do edycji.</p><?php endif;?></section>
<section class="panel" id="jezyki"><div class="top"><div><h2>Języki menu</h2><p class="muted">Właściwe centrum tłumaczeń dla PL → EN, DE i ES. Bez drugiego starego modułu PL/EN.</p></div><a class="button" href="/admin/polana-v16-tlumaczenia.php?area=menu&menu_id=<?=$menuId?>&lang=all">Tłumacz / popraw / zatwierdź</a></div>
<div class="grid4"><div class="note"><strong>PL</strong><div class="muted">Oryginał</div></div><?php foreach(['en','de','es'] as $lang):$r=$translation[$lang];?><div class="note"><strong><?=strtoupper($lang)?></strong><div class="muted"><?=$r['total']-$r['missing']?> / <?=$r['total']?> pól w bazie</div><span class="small <?=$r['missing']?'warning':'ok'?>"><?=$r['missing']?'Brak '.$r['missing'].' pól':'Komplet pól'?></span></div><?php endforeach;?></div>
<div class="chips" style="margin:14px 0"><span class="chip">Oczekuje: <?=$queue['pending']?></span><span class="chip">Do zatwierdzenia: <?=$queue['review']?></span><span class="chip">Zatwierdzone w kolejce: <?=$queue['approved']?></span><span class="chip">Błędy: <?=$queue['failed']?></span></div>
<p class="muted">Uwaga: komplet wypełnionych pól nie oznacza, że nowe tłumaczenia zostały zatwierdzone. Jeżeli źródło PL się zmieniło, sprawdź kolejkę. Brakujące teksty i ręczne korekty obsługuj wyłącznie w Centrum Tłumaczeń.</p>
</section>
<section class="panel" id="eksport"><h2>Word / PDF / podgląd menu</h2><div class="grid4">
<?php foreach(['pl'=>'Polski','en'=>'English','de'=>'Deutsch','es'=>'Español'] as $lang=>$label):$draft=($lang!=='pl'&&($translation[$lang]['missing']??0)>0);?>
<div class="panel" style="margin:0;padding:12px"><strong><?=pm165_html($label)?></strong><?php if($draft):?><p class="small warning">Brak pól: eksport roboczy</p><?php else:?><p class="small ok">Brak pustych pól w tej karcie</p><?php endif;?><div class="toolbar"><?php if($role==='admin'):?><a class="button alt" href="/admin/tklm-word-export-v5.php?menu_id=<?=$menuId?>&lang=<?=$lang?><?=$draft?'&draft=1':''?>">Word</a><a class="button alt" target="_blank" rel="noopener" href="/admin/tklm-pdf-export-v6.php?menu_id=<?=$menuId?>&lang=<?=$lang?><?=$draft?'&draft=1':''?>">PDF</a><?php else:?><span class="muted small">Eksport: administrator</span><?php endif;?></div></div><?php endforeach;?></div>
<p class="muted">Word/PDF zachowują podpis Andrzeja Polana i aktualne ustawienia generatora. Eksport obejmuje typy kart zgodnie z istniejącym modułem.</p>
<div class="toolbar"><a class="button alt" href="/menu?kind=<?=pm165_html($type)?>" target="_blank" rel="noopener">Karta PL na stronie</a><?php foreach(['en','de','es'] as $lang):?><a class="button alt" href="/<?=$lang?>/menu?kind=<?=pm165_html($type)?>" target="_blank" rel="noopener">Podgląd <?=strtoupper($lang)?></a><?php endforeach;?></div></section>
<?php endif;?>
</main>
<script>
(function(){
const input=document.getElementById('pmm-search');if(!input)return;
const rows=[...document.querySelectorAll('[data-search]')];
const normalize=s=>(s||'').toLocaleLowerCase('pl-PL').normalize('NFD').replace(/[\u0300-\u036f]/g,'').trim();
input.addEventListener('input',()=>{
const q=normalize(input.value);rows.forEach(row=>{row.hidden=!!q && !normalize(row.getAttribute('data-search')).includes(q);row.classList.toggle('hidden',row.hidden)});
document.querySelectorAll('section.category').forEach(cat=>{const shown=[...cat.querySelectorAll('[data-search]')].some(x=>!x.hidden);cat.style.opacity=(!shown&&q)?'.55':'';});
});
})();
</script>
<?php render_admin_footer(); ?>