| 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/functions.php';
require_login();
if (!in_array((string)(admin_user()['role']??''),['admin','editor'],true)) {http_response_code(403);exit('Brak uprawnień do menu.');}
require_once dirname(__DIR__).'/includes/polana_v18_menu.php';
/** Diagnostic for legacy databases where menu_type exists but its ENUM omits wine. */
function polana_admin_menu_kind_allowed(string $kind): bool
{
if (!polana_schema_ready()) return false;
try {
$row = db()->query("SHOW COLUMNS FROM `menus` LIKE 'menu_type'")->fetch(PDO::FETCH_ASSOC);
if (!$row) return false;
$type = (string)($row['Type'] ?? '');
if (!preg_match('/^enum\((.*)\)$/is', $type, $m)) return true;
preg_match_all("/'([^']+)'/", $m[1], $matches);
return in_array($kind, $matches[1], true);
} catch (Throwable $e) {
error_log('[POLANA_MENU_TYPES] ' . $e->getMessage());
return false;
}
}
function polana_admin_wine_ready(): bool
{
if (!polana_admin_menu_kind_allowed('wine')) return false;
try {
$required = [
'menu_categories' => ['name_en'],
'menu_items' => ['name_en', 'description_en', 'ingredients_en', 'allergens_en'],
];
foreach ($required as $table => $columns) {
foreach ($columns as $column) {
$stmt = db()->query("SHOW COLUMNS FROM `$table` LIKE " . db()->quote($column));
if (!$stmt->fetch(PDO::FETCH_ASSOC)) return false;
}
}
return true;
} catch (Throwable $e) {
error_log('[POLANA_WINE_READY] ' . $e->getMessage());
return false;
}
}
$wineAllowed = polana_admin_wine_ready();
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
verify_csrf();
if (!polana_schema_ready()) {
flash('error', 'Brak kolumny menu_type. Uruchom migrację PL/EN i ponów tworzenie karty.');
redirect('/admin/polana-bilingual-install.php');
}
$action = (string)($_POST['action'] ?? '');
if (in_array($action,['rename','publish','draft','archive','trash','restore','unarchive'],true)) {
try {flash('success',pv18_menu_action($action,$_POST));}
catch(Throwable $e) {flash('error',$e->getMessage());}
redirect('/admin/menus.php');
}
if ($action === 'new') {
$name = trim((string)($_POST['name'] ?? 'Nowe menu'));
$validFrom = (string)($_POST['valid_from'] ?? date('Y-m-01'));
$kind = polana_menu_type((string)($_POST['menu_type'] ?? 'food'));
if (!polana_admin_menu_kind_allowed($kind) || ($kind === 'wine' && !$wineAllowed)) {
flash('error', 'Baza nie obsługuje jeszcze typu „' . polana_type_label($kind, 'pl') . '”. Administrator musi uruchomić naprawę PL/EN.');
redirect('/admin/menus.php');
}
$slug = 'menu-' . date('Ymd-His') . '-' . bin2hex(random_bytes(3));
try {
$stmt = db()->prepare(
'INSERT INTO menus (name, slug, status, valid_from, intro, seo_title, seo_description, menu_type)
VALUES (?, ?, "draft", ?, ?, ?, ?, ?)'
);
$stmt->execute([
$name ?: 'Nowe menu',
$slug,
$validFrom ?: null,
'Poznaj nasze aktualne menu.',
'Menu | ' . setting('site_name', APP_NAME),
'Aktualne menu restauracji ' . setting('site_name', APP_NAME) . '.',
$kind,
]);
$id = (int)db()->lastInsertId();
admin_log('menu_create', 'Utworzono menu #' . $id . ' (' . $kind . ')');
flash('success', 'Utworzono kartę: ' . polana_type_label($kind, 'pl') . '. Dodaj kategorię i pozycje.');
redirect('/admin/menu.php?id=' . $id);
} catch (Throwable $e) {
error_log('[POLANA_MENU_CREATE] ' . $e->getMessage());
flash('error', 'Nie udało się utworzyć karty „' . polana_type_label($kind, 'pl') . '”. Sprawdź migrację PL/EN lub log PHP (kod ' . (string)$e->getCode() . ').');
redirect('/admin/menus.php');
}
}
if ($action === 'copy') {
$sourceId = (int)($_POST['source_id'] ?? 0);
$stmt = db()->prepare('SELECT * FROM menus WHERE id = ?');
$stmt->execute([$sourceId]);
$source = $stmt->fetch();
if (!$source) {
flash('error', 'Nie znaleziono menu do skopiowania.');
redirect('/admin/menus.php');
}
$sourceType = polana_menu_type((string)($source['menu_type'] ?? 'food'));
if (!polana_admin_menu_kind_allowed($sourceType) || ($sourceType === 'wine' && !$wineAllowed)) {
flash('error', 'Nie można skopiować tej karty — najpierw napraw obsługę typu ' . polana_type_label($sourceType, 'pl') . '.');
redirect('/admin/menus.php');
}
db()->beginTransaction();
try {
$newName = trim((string)($_POST['new_name'] ?? ('Kopia – ' . $source['name'])));
$validFrom = (string)($_POST['copy_valid_from'] ?? date('Y-m-01'));
$stmt = db()->prepare(
'INSERT INTO menus
(name, slug, status, valid_from, valid_to, intro, pdf_path, seo_title, seo_description, menu_type)
VALUES (?, ?, "draft", ?, NULL, ?, ?, ?, ?, ?)'
);
$stmt->execute([
$newName ?: ('Kopia – ' . $source['name']),
'menu-' . date('Ymd-His') . '-' . random_int(100, 999),
$validFrom ?: null,
$source['intro'],
$source['pdf_path'],
$source['seo_title'],
$source['seo_description'],
$source['menu_type'] ?? 'food',
]);
$newMenuId = (int)db()->lastInsertId();
$catStmt = db()->prepare(
'SELECT * FROM menu_categories WHERE menu_id = ? ORDER BY sort_order, id'
);
$catStmt->execute([$sourceId]);
foreach ($catStmt->fetchAll() as $cat) {
$insertCat = db()->prepare(
'INSERT INTO menu_categories (menu_id, name, description, sort_order, active, name_en, name_es, name_de)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)'
);
$insertCat->execute([
$newMenuId,
$cat['name'],
$cat['description'],
$cat['sort_order'],
$cat['active'],
$cat['name_en'] ?? null,
$cat['name_es'] ?? null,
$cat['name_de'] ?? null,
]);
$newCatId = (int)db()->lastInsertId();
$itemStmt = db()->prepare(
'SELECT * FROM menu_items WHERE category_id = ? ORDER BY sort_order, id'
);
$itemStmt->execute([(int)$cat['id']]);
foreach ($itemStmt->fetchAll() as $item) {
$insertItem = db()->prepare(
'INSERT INTO menu_items
(category_id, name, description, ingredients, ingredients_en, price, price_text, weight, allergens, allergens_en,
image_path, available, active, featured, sort_order, name_en, description_en, name_es, name_de, description_es, description_de, ingredients_es, ingredients_de, allergens_es, allergens_de)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'
);
$insertItem->execute([
$newCatId,
$item['name'],
$item['description'],
$item['ingredients'] ?? null,
$item['ingredients_en'] ?? null,
$item['price'],
$item['price_text'],
$item['weight'],
$item['allergens'],
$item['allergens_en'] ?? null,
$item['image_path'],
$item['available'],
$item['active'],
$item['featured'],
$item['sort_order'],
$item['name_en'] ?? null,
$item['description_en'] ?? null,
$item['name_es'] ?? null,
$item['name_de'] ?? null,
$item['description_es'] ?? null,
$item['description_de'] ?? null,
$item['ingredients_es'] ?? null,
$item['ingredients_de'] ?? null,
$item['allergens_es'] ?? null,
$item['allergens_de'] ?? null,
]);
}
}
db()->commit();
admin_log('menu_copy', 'Skopiowano menu #' . $sourceId . ' do #' . $newMenuId);
flash('success', 'Menu zostało skopiowane. Możesz zmieniać ceny i pozycje.');
redirect('/admin/menu.php?id=' . $newMenuId);
} catch (Throwable $e) {
if (db()->inTransaction()) db()->rollBack();
error_log('[POLANA_MENU_COPY] ' . $e->getMessage());
flash('error', 'Nie udało się skopiować menu. Sprawdź log PHP (kod ' . (string)$e->getCode() . ').');
redirect('/admin/menus.php');
}
}
}
/* POLANA V18: paginated and searchable list, with reversible trash. */
$trashReady=pv18_menu_trash_ready();
$q=trim((string)($_GET['q']??''));if(strlen($q)>100)$q=substr($q,0,100);
$status=(string)($_GET['status']??'current');
if(!in_array($status,['current','all','draft','published','archived','trash'],true))$status='current';
$kind=(string)($_GET['type']??'all');
if(!in_array($kind,['all','food','soft','wine'],true))$kind='all';
$perPage=(int)($_GET['per_page']??20);if(!in_array($perPage,[20,50,100],true))$perPage=20;
$page=max(1,(int)($_GET['page']??1));
$join=$trashReady?' LEFT JOIN polana_menu_trash t ON t.menu_id=m.id ':' ';
$trashCol=$trashReady?'t.deleted_at AS deleted_at':'NULL AS deleted_at';
$conditions=[];$params=[];
if($trashReady){
if($status==='trash')$conditions[]='t.menu_id IS NOT NULL';
else $conditions[]='t.menu_id IS NULL';
} elseif($status==='trash')$conditions[]='1=0';
if($status==='current')$conditions[]="m.status IN ('draft','published')";
elseif(in_array($status,['draft','published','archived'],true))$conditions[]='m.status=?';
if(in_array($status,['draft','published','archived'],true))$params[]=$status;
if($kind!=='all'){$conditions[]='m.menu_type=?';$params[]=$kind;}
if($q!==''){
if(ctype_digit($q)){$conditions[]='(m.name LIKE ? OR m.id=?)';$params[]='%'.$q.'%';$params[]=(int)$q;}
else {$conditions[]='m.name LIKE ?';$params[]='%'.$q.'%';}
}
$where=$conditions?' WHERE '.implode(' AND ',$conditions):'';
$countStmt=db()->prepare('SELECT COUNT(*) FROM menus m'.$join.$where);$countStmt->execute($params);
$total=(int)$countStmt->fetchColumn();$pages=max(1,(int)ceil($total/$perPage));$page=min($page,$pages);
$offset=($page-1)*$perPage;
$listSql='SELECT m.id,m.name,m.menu_type,m.status,m.valid_from,m.valid_to,m.updated_at,'.$trashCol.' FROM menus m'.$join.$where.' ORDER BY m.updated_at DESC,m.id DESC LIMIT '.$perPage.' OFFSET '.$offset;
$listStmt=db()->prepare($listSql);$listStmt->execute($params);$menus=$listStmt->fetchAll(PDO::FETCH_ASSOC);
$copyMenus=db()->query("SELECT id,name,menu_type FROM menus WHERE status IN ('published','draft','archived') ORDER BY id DESC LIMIT 120")->fetchAll(PDO::FETCH_ASSOC);
function pv18_list_link(int $n):string {
$params=$_GET;$params['page']=$n;
return '/admin/menus.php?'.http_build_query($params);
}
render_admin_header('Wersje menu — Polana V18');
?>
<style>
.pv18m{max-width:1450px;margin:0 auto 70px;padding:14px;color:#213428}.pv18m *{box-sizing:border-box}.pv18m .panel{background:white;border:1px solid #dbe5da;border-radius:16px;padding:18px;margin:14px 0}.pv18m .cols{display:grid;grid-template-columns:1fr 1fr;gap:14px}.pv18m .fields{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:11px}.pv18m label{font-size:13px;font-weight:700}.pv18m input,.pv18m select{width:100%;border:1px solid #bdcbbb;border-radius:9px;padding:10px;font:inherit;min-height:40px}.pv18m .btn{border:0;background:#234e32;color:#fff;padding:9px 12px;border-radius:9px;text-decoration:none;display:inline-flex;align-items:center;justify-content:center;font-weight:650;font:inherit;cursor:pointer;min-height:38px}.pv18m .btn.alt{background:#ecf3ea;color:#24402d;border:1px solid #cbdccb}.pv18m .btn.warn{background:#854e20}.pv18m .btn.danger{background:#a03434}.pv18m .btn:focus-visible{outline:3px solid #a0cb9d;outline-offset:2px}.pv18m .toolbar{display:flex;align-items:center;gap:8px;flex-wrap:wrap}.pv18m .filters{display:grid;grid-template-columns:2fr 1fr 1fr 1fr auto;gap:10px;align-items:end}.pv18m .scroll{overflow-x:auto}.pv18m table{width:100%;border-collapse:collapse;min-width:870px}.pv18m td,.pv18m th{border-bottom:1px solid #e4ebe3;padding:12px 9px;text-align:left;vertical-align:top}.pv18m .sub{font-size:12px;color:#637267}.pv18m .actions{display:flex;gap:6px;flex-wrap:wrap;align-items:center}.pv18m .actions form{margin:0}.pv18m .actions .btn{font-size:12px;min-height:34px;padding:7px 9px}.pv18m .badge{display:inline-block;border-radius:99px;background:#e9f2e7;color:#244e2d;padding:4px 8px;font-size:12px;font-weight:700}.pv18m details{border:1px solid #d8e5d5;border-radius:9px;padding:7px 9px}.pv18m summary{cursor:pointer;font-size:12px;font-weight:700}.pv18m .pages{display:flex;flex-wrap:wrap;gap:8px;margin-top:15px}.pv18m .pages a{padding:8px 12px;border:1px solid #d4e2d1;border-radius:8px}.pv18m .note{background:#f8f5e9;border-left:4px solid #bda05e;padding:13px;border-radius:8px}.pv18m h1{margin:0 0 7px;font-size:clamp(26px,4vw,36px)}.pv18m h2{margin:0 0 14px;font-size:20px}@media(max-width:850px){.pv18m .cols{grid-template-columns:1fr}.pv18m .filters{grid-template-columns:1fr 1fr}.pv18m .fields{grid-template-columns:1fr}}@media(max-width:530px){.pv18m .filters{grid-template-columns:1fr}.pv18m{padding:9px}}
</style>
<div class="pv18m">
<h1>Menu — wszystkie karty</h1>
<p>Twórz, kopiuj, publikuj, archiwizuj i przenoś do kosza. QR prowadzi do stałego adresu strony, nie do konkretnej karty.</p>
<div class="toolbar"><a class="btn alt" href="/admin/polana-menu-centrum.php">Edytor dań i cen</a><a class="btn alt" href="/admin/polana-indexnow.php">IndexNow / Bing</a><a class="btn alt" href="/admin/polana-zarzadzanie.php">Centrum zarządzania</a></div>
<?php if(!$trashReady):?><div class="note">Kosz menu nie jest jeszcze aktywny. Uruchom instalator V18.0. Pozostałe funkcje nadal działają.</div><?php endif;?>
<?php if(!$wineAllowed):?><div class="note">Baza nie obsługuje jeszcze wszystkich typów kart. <a href="/admin/polana-bilingual-install.php">Diagnostyka PL/EN/DE/ES</a>.</div><?php endif;?>
<div class="cols">
<section class="panel"><h2>Nowa karta</h2><form method="post" class="fields"><?=csrf_field()?><input type="hidden" name="action" value="new"><label>Nazwa<input name="name" value="Menu <?=e(date('m/Y'))?>" required maxlength="150"></label><label>Rodzaj<select name="menu_type"><?php foreach(polana_types() as $k=>$labels):?><option value="<?=e($k)?>"><?=e($labels['pl'])?></option><?php endforeach;?></select></label><label>Obowiązuje od<input type="date" name="valid_from" value="<?=e(date('Y-m-01'))?>"></label><div class="toolbar"><button class="btn">Utwórz szkic</button></div></form></section>
<section class="panel"><h2>Kopiuj kartę</h2><form method="post" class="fields"><?=csrf_field()?><input type="hidden" name="action" value="copy"><label>Karta źródłowa<select name="source_id" required><?php foreach($copyMenus as $m):?><option value="<?=(int)$m['id']?>"><?=e($m['name'])?> (#<?=(int)$m['id']?>)</option><?php endforeach;?></select></label><label>Nowa nazwa<input name="new_name" maxlength="150" value="Menu <?=e(date('m/Y',strtotime('+1 month')))?>"></label><label>Obowiązuje od<input type="date" name="copy_valid_from" value="<?=e(date('Y-m-01',strtotime('+1 month')))?>"></label><div class="toolbar"><button class="btn" <?=$copyMenus?'':'disabled'?>>Skopiuj jako szkic</button></div></form></section>
</div>
<section class="panel"><h2>Wyszukiwanie i filtry</h2><form method="get" class="filters"><label>Szukaj nazwy lub ID<input type="search" name="q" value="<?=e($q)?>" placeholder="np. 10/2026 lub 42"></label><label>Status<select name="status"><?php foreach(['current'=>'Bieżące','all'=>'Wszystkie bez kosza','draft'=>'Szkice','published'=>'Opublikowane','archived'=>'Archiwum','trash'=>'Kosz'] as $key=>$val):?><option value="<?=e($key)?>" <?=$status===$key?'selected':''?>><?=e($val)?></option><?php endforeach;?></select></label><label>Rodzaj<select name="type"><?php foreach(['all'=>'Wszystkie','food'=>'Dania','soft'=>'Napoje','wine'=>'Wina'] as $key=>$val):?><option value="<?=e($key)?>" <?=$kind===$key?'selected':''?>><?=e($val)?></option><?php endforeach;?></select></label><label>Na stronę<select name="per_page"><?php foreach([20,50,100] as $n):?><option value="<?=$n?>" <?=$perPage===$n?'selected':''?>><?=$n?></option><?php endforeach;?></select></label><button class="btn">Filtruj</button></form><p class="sub">Znaleziono <?=$total?> kart · strona <?=$page?> z <?=$pages?>.</p></section>
<section class="panel"><div class="scroll"><table><thead><tr><th>Nazwa / numer</th><th>Rodzaj</th><th>Status</th><th>Od / do</th><th>Zmiana</th><th>Akcje</th></tr></thead><tbody>
<?php if(!$menus):?><tr><td colspan="6">Brak kart dla wybranych filtrów.</td></tr><?php endif;?>
<?php foreach($menus as $m):$id=(int)$m['id'];$trashed=!empty($m['deleted_at']);?>
<tr><td><strong><?=e($m['name'])?></strong><div class="sub">#<?=$id?></div></td><td><?=e(polana_type_label((string)$m['menu_type'],'pl'))?></td><td><span class="badge"><?=$trashed?'Kosz':e($m['status'])?></span></td><td><?=e((string)($m['valid_from']??'—'))?><div class="sub">do <?=e((string)($m['valid_to']??'—'))?></div></td><td class="sub"><?=e((string)$m['updated_at'])?></td><td>
<div class="actions">
<?php if($trashed):?>
<?php if((admin_user()['role']??'')==='admin'):?><form method="post"><?=csrf_field()?><input type="hidden" name="action" value="restore"><input type="hidden" name="id" value="<?=$id?>"><button class="btn">Przywróć jako szkic</button></form><?php endif;?>
<?php else:?>
<a class="btn" href="/admin/menu.php?id=<?=$id?>">Edytuj</a>
<?php if($m['status']!=='published'):?><form method="post" onsubmit="return confirm('Opublikować kartę? Jeśli istnieją karty z nakładającymi się datami, trzeba świadomie zaznaczyć zastąpienie.')"><?=csrf_field()?><input type="hidden" name="action" value="publish"><input type="hidden" name="id" value="<?=$id?>"><label style="font-weight:400;font-size:11px;display:flex;align-items:center;gap:3px"><input type="checkbox" name="replace_conflicts" value="1" style="width:auto;min-height:0">Zastąp kolidujące</label><button class="btn">Publikuj</button></form><?php endif;?>
<?php if($m['status']==='published'||$m['status']==='draft'):?><form method="post" onsubmit="return confirm('Zarchiwizować kartę?')"><?=csrf_field()?><input type="hidden" name="action" value="archive"><input type="hidden" name="id" value="<?=$id?>"><button class="btn alt">Archiwizuj</button></form><?php else:?><form method="post"><?=csrf_field()?><input type="hidden" name="action" value="unarchive"><input type="hidden" name="id" value="<?=$id?>"><button class="btn alt">Do szkiców</button></form><?php endif;?>
<?php if($m['status']!=='published' && (admin_user()['role']??'')==='admin' && $trashReady):?><form method="post" onsubmit="return confirm('Przenieść kartę do kosza? Można ją później przywrócić.')"><?=csrf_field()?><input type="hidden" name="action" value="trash"><input type="hidden" name="id" value="<?=$id?>"><button class="btn danger">Do kosza</button></form><?php endif;?>
<details><summary>Nazwa i daty</summary><form method="post" style="min-width:230px;padding:8px 0;display:grid;gap:7px"><?=csrf_field()?><input type="hidden" name="action" value="rename"><input type="hidden" name="id" value="<?=$id?>"><label>Nazwa<input name="name" value="<?=e($m['name'])?>" maxlength="150" required></label><label>Od<input type="date" name="valid_from" value="<?=e((string)$m['valid_from'])?>"></label><label>Do<input type="date" name="valid_to" value="<?=e((string)$m['valid_to'])?>"></label><button class="btn">Zapisz</button></form></details>
<?php endif;?>
</div>
<div class="actions" style="margin-top:7px"><a class="btn alt" href="/admin/menu-export.php?id=<?=$id?>&format=docx&lang=pl">Word PL</a><a class="btn alt" target="_blank" rel="noopener" href="/menu-druk.php?menu_id=<?=$id?>&kind=<?=e((string)$m['menu_type'])?>&lang=pl">PDF PL</a></div>
</td></tr><?php endforeach;?></tbody></table></div>
<?php if($pages>1):?><nav class="pages" aria-label="Strony kart menu"><?php if($page>1):?><a href="<?=e(pv18_list_link($page-1))?>">← Poprzednia</a><?php endif;?><?php for($n=max(1,$page-2);$n<=min($pages,$page+2);$n++):?><a href="<?=e(pv18_list_link($n))?>" <?=$n===$page?'aria-current="page"':''?>><?=$n?></a><?php endfor;?><?php if($page<$pages):?><a href="<?=e(pv18_list_link($page+1))?>">Następna →</a><?php endif;?></nav><?php endif;?>
</section>
<p class="note">Kosz nie usuwa danych z bazy. Przywracanie zawsze tworzy szkic. Publikacja z zastąpieniem archiwizuje wyłącznie karty tego samego rodzaju, których daty się nakładają. Nie zmieniamy linku QR.</p>
</div>
<?php render_admin_footer(); ?>