| 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);
/**
* TKLM Polana: Plan sali na wybrany dzień + Excel (XLSX) + wydruk/PDF.
* Nie zmienia bazy. Wydruk do PDF realizuje przeglądarka (Zapisz jako PDF),
* dzięki czemu polskie znaki działają bez dodatkowych bibliotek na hostingu.
*/
require_once dirname(__DIR__) . '/includes/functions.php';
require_login();
ensure_reservations_schema();
header('Cache-Control: private, no-store, max-age=0');
header('Pragma: no-cache');
header('X-Robots-Tag: noindex, nofollow, noarchive');
header('X-Content-Type-Options: nosniff');
$rawDate = trim((string)($_GET['date'] ?? date('Y-m-d')));
$day = preg_match('/^\d{4}-\d{2}-\d{2}$/D', $rawDate)
? DateTimeImmutable::createFromFormat('!Y-m-d', $rawDate) : false;
if (!$day || $day->format('Y-m-d') !== $rawDate) {
http_response_code(400);
exit('Nieprawidłowa data. Podaj datę w formacie RRRR-MM-DD.');
}
$date = $day->format('Y-m-d');
$dateDisplay = $day->format('d.m.Y');
$format = trim((string)($_GET['format'] ?? 'html'));
if (!in_array($format, ['html', 'xlsx', 'print'], true)) {
http_response_code(400);
exit('Nieprawidłowy format raportu.');
}
$stmt = db()->prepare("SELECT * FROM reservations WHERE reservation_date=? AND status NOT IN ('cancelled','rejected','no_show') ORDER BY reservation_time ASC, id ASC");
$stmt->execute([$date]);
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
// Nazwy kelnerów są opcjonalne; brak rozszerzonych pól tabeli nie blokuje raportu.
$waiterIds = [];
foreach ($rows as $row) {
if ((int)($row['assigned_waiter_user_id'] ?? 0) > 0) {
$waiterIds[] = (int)$row['assigned_waiter_user_id'];
}
}
$waiterNames = [];
if ($waiterIds) {
$waiterIds = array_values(array_unique($waiterIds));
$ph = implode(',', array_fill(0, count($waiterIds), '?'));
$q = db()->prepare('SELECT id, name, username FROM users WHERE id IN (' . $ph . ')');
$q->execute($waiterIds);
foreach ($q->fetchAll(PDO::FETCH_ASSOC) as $u) {
$waiterNames[(int)$u['id']] = (string)($u['name'] ?: $u['username']);
}
}
$seenContacts = [];
$hours = [];
$counts = [
'requests' => 0, 'guests' => 0, 'new' => 0, 'confirmed' => 0,
'duplicates' => 0, 'duplicate_guests' => 0, 'estimated_guests' => 0,
];
foreach ($rows as &$row) {
$hour = substr((string)$row['reservation_time'], 0, 5);
if (!isset($hours[$hour])) $hours[$hour] = ['requests' => 0, 'guests' => 0, 'duplicates' => 0];
$keys = reservation_contact_keys($row);
$duplicate = false;
foreach ($keys as $key) {
if (isset($seenContacts[$hour . '|' . $key])) $duplicate = true;
}
foreach ($keys as $key) $seenContacts[$hour . '|' . $key] = true;
$row['_duplicate'] = $duplicate;
$row['_status_label'] = reservation_status_label((string)($row['status'] ?? ''));
$row['_waiter_name'] = $waiterNames[(int)($row['assigned_waiter_user_id'] ?? 0)] ?? '';
$guests = max(0, (int)$row['guests']);
$counts['requests']++;
$counts['guests'] += $guests;
if (($row['status'] ?? '') === 'new') $counts['new']++;
if (($row['status'] ?? '') === 'confirmed') $counts['confirmed']++;
if ($duplicate) {
$counts['duplicates']++;
$counts['duplicate_guests'] += $guests;
$hours[$hour]['duplicates']++;
}
$hours[$hour]['requests']++;
$hours[$hour]['guests'] += $guests;
}
unset($row);
$counts['estimated_guests'] = $counts['guests'] - $counts['duplicate_guests'];
// Zestawienie miesięczne dla kalendarza (bez danych osobowych).
$monthFirst = $day->modify('first day of this month');
$monthLast = $day->modify('last day of this month');
$calendarStmt = db()->prepare(
"SELECT reservation_date, COUNT(*) AS total, COALESCE(SUM(guests), 0) AS people
FROM reservations WHERE reservation_date BETWEEN ? AND ?
AND status NOT IN ('cancelled','rejected','no_show')
GROUP BY reservation_date"
);
$calendarStmt->execute([$monthFirst->format('Y-m-d'), $monthLast->format('Y-m-d')]);
$calendarReservations = [];
foreach ($calendarStmt->fetchAll(PDO::FETCH_ASSOC) as $calRow) {
$calendarReservations[(string)$calRow['reservation_date']] = [
'count' => (int)$calRow['total'], 'people' => (int)$calRow['people'],
];
}
$calendarMonthName = [1=>'styczeń', 'luty','marzec','kwiecień','maj','czerwiec',
'lipiec','sierpień','wrzesień','październik','listopad','grudzień'][(int)$monthFirst->format('n')];
$calendarLead = (int)$monthFirst->format('N') - 1;
$calendarDays = (int)$monthLast->format('j');
$reportUrl = static function (string $formatValue) use ($date): string {
return '/admin/reservations-day.php?' . http_build_query(['date' => $date, 'format' => $formatValue]);
};
if ($format === 'xlsx') {
require_once dirname(__DIR__) . '/includes/polana_xlsx_export.php';
try {
$binary = polana_reservation_xlsx($date, $rows, $hours, $counts);
if (strlen($binary) < 1500) throw new RuntimeException('Wynik XLSX jest zbyt mały.');
} catch (Throwable $e) {
error_log('[POLANA REPORT XLSX] ' . get_class($e) . ': ' . $e->getMessage());
http_response_code(500);
exit('Nie udało się wygenerować pliku Excel. Sprawdź log PHP.');
}
if (session_status() === PHP_SESSION_ACTIVE) session_write_close();
header('Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
header('Content-Disposition: attachment; filename="Polana_plan_sali_' . $date . '.xlsx"');
header('Content-Length: ' . strlen($binary));
echo $binary;
exit;
}
if ($format === 'print'):
if (session_status() === PHP_SESSION_ACTIVE) session_write_close();
?><!doctype html>
<html lang="pl"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>Plan sali – Moja Polana Smaków – <?= e($dateDisplay) ?></title>
<style>
body{font-family:Arial,Helvetica,sans-serif;background:#fff;color:#202a23;margin:24px auto;max-width:1300px;font-size:12px}
header{border-bottom:3px solid #31533d;padding:0 0 12px;margin-bottom:14px;display:flex;justify-content:space-between;align-items:end}
h1{font-size:23px;margin:0 0 3px}h2{font-size:16px;margin:20px 0 9px}p{margin:5px 0}.muted{color:#5d6d61}
.stats{display:flex;flex-wrap:wrap;gap:9px;margin:14px 0}.stat{border:1px solid #d1dace;padding:9px 14px;border-radius:5px;min-width:125px}.stat b{font-size:19px;display:block;margin-top:4px}
.warning{border:1px solid #dcaa59;background:#fff7e6;padding:10px 12px;margin:12px 0}
table{width:100%;border-collapse:collapse;table-layout:fixed}th{background:#e5ece4;text-align:left;border-bottom:2px solid #66836b;font-size:11px}td,th{padding:7px 6px;vertical-align:top;overflow-wrap:anywhere;border-bottom:1px solid #dce3dc}.duplicate{background:#fff0d9}.new{background:#faf8ef}.badge{font-size:10px;font-weight:bold}.print-actions{display:flex;gap:10px;margin:0 0 14px}.print-actions button,.print-actions a{font-size:14px;border:0;background:#31533d;color:#fff;padding:9px 15px;border-radius:5px;text-decoration:none;cursor:pointer}.nowrap{white-space:nowrap}small{font-size:10px}footer{margin-top:16px;font-size:10px;color:#4c5b4d}
@page{size:A4 landscape;margin:10mm}@media print{body{max-width:none;margin:0;font-size:10px}.print-actions{display:none}th{print-color-adjust:exact;-webkit-print-color-adjust:exact}.duplicate,.new{print-color-adjust:exact;-webkit-print-color-adjust:exact}td,th{padding:5px 4px}h1{font-size:19px}thead{display:table-header-group}tr{break-inside:avoid}}
</style></head><body>
<div class="print-actions"><button type="button" onclick="window.print()">Drukuj / Zapisz jako PDF</button><a href="<?= e($reportUrl('html')) ?>">← Wróć do planu</a></div>
<header><div><h1>MOJA POLANA SMAKÓW</h1><p>PLAN SALI I LISTA REZERWACJI · <?= e($dateDisplay) ?></p></div><div class="muted">Wygenerowano: <?= e(date('d.m.Y H:i')) ?></div></header>
<div class="stats"><div class="stat">Zgłoszenia<b><?= $counts['requests'] ?></b></div><div class="stat">Goście (wszystkie wpisy)<b><?= $counts['guests'] ?></b></div><div class="stat">Potwierdzone<b><?= $counts['confirmed'] ?></b></div><div class="stat">Do potwierdzenia<b><?= $counts['new'] ?></b></div><div class="stat">Osoby orientacyjnie bez dubli<b><?= $counts['estimated_guests'] ?></b></div></div>
<?php if ($counts['duplicates']): ?><div class="warning"><strong>Uwaga: <?= $counts['duplicates'] ?> możliwych powtórzeń (<?= $counts['duplicate_guests'] ?> osób).</strong> Zweryfikuj wpisy przed rozstawieniem stolików. Niczego automatycznie nie usunięto.</div><?php endif; ?>
<h2>Obciążenie według godzin</h2>
<table style="table-layout:auto;max-width:640px"><thead><tr><th>Godzina</th><th>Zgłoszenia</th><th>Osoby</th><th>Możliwe duplikaty</th></tr></thead><tbody><?php foreach ($hours as $time => $h): ?><tr><td><?= e($time) ?></td><td><?= $h['requests'] ?></td><td><?= $h['guests'] ?></td><td><?= $h['duplicates'] ?></td></tr><?php endforeach; ?><?php if (!$hours): ?><tr><td colspan="4">Brak rezerwacji.</td></tr><?php endif; ?></tbody></table>
<h2>Lista gości i przygotowanie sali</h2>
<table><colgroup><col style="width:7%"><col style="width:16%"><col style="width:5%"><col style="width:11%"><col style="width:8%"><col style="width:10%"><col style="width:11%"><col style="width:24%"><col style="width:8%"></colgroup>
<thead><tr><th>Godz.</th><th>Gość</th><th>Os.</th><th>Status</th><th>Stolik</th><th>Kelner</th><th>Telefon</th><th>Uwagi / przygotowanie</th><th>Kontrola</th></tr></thead><tbody>
<?php foreach ($rows as $r): ?><tr class="<?= $r['_duplicate'] ? 'duplicate' : (($r['status'] ?? '') === 'new' ? 'new' : '') ?>"><td class="nowrap"><?= e(substr((string)$r['reservation_time'],0,5)) ?></td><td><strong><?= e((string)$r['guest_name']) ?></strong><br><small><?= e((string)($r['reservation_code'] ?? '')) ?></small></td><td><strong><?= (int)$r['guests'] ?></strong></td><td><?= e((string)$r['_status_label']) ?></td><td><?= e((string)($r['table_label'] ?? '')) ?></td><td><?= e((string)$r['_waiter_name']) ?></td><td><?= e((string)($r['phone'] ?? '')) ?></td><td><?= nl2br(e(trim((string)($r['notes'] ?? '') . "\n" . (string)($r['internal_note'] ?? '')))) ?></td><td><?= $r['_duplicate'] ? '<span class="badge">SPRAWDŹ DUBEL</span>' : '' ?></td></tr><?php endforeach; ?><?php if (!$rows): ?><tr><td colspan="9">Brak rezerwacji na ten dzień.</td></tr><?php endif; ?>
</tbody></table>
<footer>Raport pomocniczy. Status „Nowa” nie oznacza potwierdzonego stolika. Duplikaty są oznaczane porównaniem terminu z numerem telefonu lub adresem e-mail. Ustal rzeczywistą liczbę gości z obsługą. Dane osobowe: dokument tylko do użytku personelu.</footer>
</body></html><?php
exit;
endif;
render_admin_header('Plan sali – ' . $dateDisplay);
?>
<div class="admin-title-row">
<div><h1>Plan sali – <?= e($dateDisplay) ?></h1><p>Dzienna lista gości, liczba osób, godziny i przygotowanie stolików.</p></div>
<a class="btn btn-light" href="/admin/reservations.php">← Wszystkie rezerwacje</a>
</div>
<section class="form-card" style="margin-bottom:20px">
<form method="get" id="polana-plan-date-form" style="display:flex;flex-wrap:wrap;align-items:end;gap:12px">
<label>Wybierz dzień<br><input type="date" name="date" id="polana-plan-date" value="<?= e($date) ?>" onchange="this.form.requestSubmit ? this.form.requestSubmit() : this.form.submit()" required></label>
<noscript><button class="btn btn-primary" type="submit">Pokaż dzień</button></noscript>
<a class="btn btn-light" href="<?= e('/admin/reservations-day.php?date='.$day->modify('-1 day')->format('Y-m-d')) ?>">← Poprzedni</a>
<a class="btn btn-light" href="<?= e('/admin/reservations-day.php?date='.$day->modify('+1 day')->format('Y-m-d')) ?>">Następny →</a>
</form>
<div class="actions" style="margin-top:14px;flex-wrap:wrap">
<a class="btn btn-primary" href="<?= e($reportUrl('xlsx')) ?>">Pobierz Excel (.xlsx)</a>
<a class="btn btn-light" href="<?= e($reportUrl('print')) ?>" target="_blank" rel="noopener">PDF / Drukuj</a>
</div>
<p class="muted">PDF: otworzy się wersja do druku. W oknie drukowania wybierz „Zapisz jako PDF”. Eksporty nie zapisują plików w katalogu strony.</p>
</section>
<section class="card" style="margin:18px 0" aria-label="Kalendarz rezerwacji">
<div style="display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap">
<h2 style="margin:0">Kalendarz · <?= e(ucfirst($calendarMonthName)) ?> <?= e($monthFirst->format('Y')) ?></h2>
<div class="actions" style="gap:8px">
<a class="btn btn-small btn-light" aria-label="Poprzedni miesiąc" href="?date=<?= e($monthFirst->modify('-1 month')->format('Y-m-d')) ?>">← Miesiąc</a>
<a class="btn btn-small btn-light" href="?date=<?= e(date('Y-m-d')) ?>">Dzisiaj</a>
<a class="btn btn-small btn-light" aria-label="Następny miesiąc" href="?date=<?= e($monthFirst->modify('+1 month')->format('Y-m-d')) ?>">Miesiąc →</a>
</div>
</div>
<p class="muted" style="margin:8px 0 16px"><span style="display:inline-block;background:#b91c1c;width:10px;height:10px;border-radius:50%"></span> Czerwony = są rezerwacje. Liczby: zgłoszenia / osoby. Kliknięcie dnia otwiera jego plan.</p>
<style>
.polana-calendar{display:grid;grid-template-columns:repeat(7,minmax(0,1fr));gap:5px}
.polana-calendar .week{padding:7px 2px;text-align:center;font-weight:700;color:#5c6960;font-size:12px}
.polana-calendar a.day{box-sizing:border-box;text-decoration:none;display:flex;flex-direction:column;justify-content:space-between;gap:4px;min-height:70px;padding:10px;border-radius:10px;border:1px solid #d7ddd6;background:#fff;color:#24372b}
.polana-calendar a.day.has{background:#fee2e2;color:#7f1d1d;border-color:#fca5a5}
.polana-calendar a.day.is-selected{outline:3px solid #344b3d;outline-offset:1px}
.polana-calendar .num{font-size:17px;font-weight:800}
.polana-calendar .sub{font-size:11px;line-height:1.3}
.polana-calendar .empty{min-height:70px}
@media(max-width:600px){.polana-calendar{gap:3px}.polana-calendar a.day{min-height:56px;padding:5px;border-radius:6px}.polana-calendar .num{font-size:13px}.polana-calendar .sub{font-size:9px}.polana-calendar .empty{min-height:56px}}
</style>
<div class="polana-calendar">
<?php foreach (['Pn','Wt','Śr','Cz','Pt','So','Nd'] as $weekday): ?><div class="week"><?= $weekday ?></div><?php endforeach; ?>
<?php for ($i=0; $i<$calendarLead; $i++): ?><div class="empty" aria-hidden="true"></div><?php endfor; ?>
<?php for ($n=1; $n<=$calendarDays; $n++):
$ymd = $monthFirst->setDate((int)$monthFirst->format('Y'), (int)$monthFirst->format('m'), $n)->format('Y-m-d');
$c = $calendarReservations[$ymd] ?? ['count'=>0,'people'=>0];
$has = $c['count'] > 0;
?>
<a class="day <?= $has ? 'has' : '' ?> <?= $date === $ymd ? 'is-selected' : '' ?>" href="?date=<?= e($ymd) ?>" <?= $date === $ymd ? 'aria-current="date"' : '' ?> aria-label="<?= e($ymd . ', ' . $c['count'] . ' rezerwacji, ' . $c['people'] . ' osób') ?>">
<span class="num"><?= $n ?></span>
<span class="sub"><?= $has ? (int)$c['count'].' rez.<br>'.(int)$c['people'].' os.' : '—' ?></span>
</a>
<?php endfor; ?>
</div>
</section>
<div class="grid-cards">
<div class="card"><h3>Zgłoszenia</h3><div class="stat"><?= $counts['requests'] ?></div></div>
<div class="card"><h3>Goście łącznie</h3><div class="stat"><?= $counts['guests'] ?></div></div>
<div class="card"><h3>Potwierdzone</h3><div class="stat"><?= $counts['confirmed'] ?></div></div>
<div class="card"><h3>Nowe</h3><div class="stat"><?= $counts['new'] ?></div></div>
<div class="card"><h3>Możliwe duplikaty</h3><div class="stat"><?= $counts['duplicates'] ?></div><p class="muted">Osoby bez powtórzeń, szacunek: <?= $counts['estimated_guests'] ?></p></div>
</div>
<?php if ($counts['duplicates']): ?><div class="flash flash-error" style="margin-top:18px">Uwaga: <?= $counts['duplicates'] ?> wpisów wygląda na duplikaty (<?= $counts['duplicate_guests'] ?> osób). Sprawdź je przed przygotowaniem sali. Niczego automatycznie nie anulowano.</div><?php endif; ?>
<section class="card" style="margin-top:20px">
<h2>Godziny i obłożenie</h2><div class="table-wrap"><table class="admin-table" style="max-width:740px"><thead><tr><th>Godzina</th><th>Rezerwacje</th><th>Goście</th><th>Powtórzenia</th></tr></thead><tbody>
<?php foreach ($hours as $hour => $h): ?><tr><td><strong><?= e($hour) ?></strong></td><td><?= $h['requests'] ?></td><td><?= $h['guests'] ?></td><td><?= $h['duplicates'] ?></td></tr><?php endforeach; ?>
<?php if (!$hours): ?><tr><td colspan="4">Brak rezerwacji na ten dzień.</td></tr><?php endif; ?></tbody></table></div>
</section>
<section class="card" style="margin-top:20px"><h2>Lista gości</h2><div class="table-wrap"><table class="admin-table"><thead><tr><th>Godzina</th><th>Gość</th><th>Os.</th><th>Status</th><th>Stolik</th><th>Kelner</th><th>Telefon</th><th>Uwagi</th><th></th></tr></thead><tbody>
<?php foreach ($rows as $r): ?><tr style="<?= $r['_duplicate'] ? 'background:#fff0d8' : '' ?>"><td><strong><?= e(substr((string)$r['reservation_time'],0,5)) ?></strong></td><td><?= e((string)$r['guest_name']) ?><?= $r['_duplicate'] ? '<br><strong style="color:#a45500">Możliwy duplikat</strong>' : '' ?></td><td><?= (int)$r['guests'] ?></td><td><?= e((string)$r['_status_label']) ?></td><td><?= e((string)($r['table_label']??'')) ?></td><td><?= e((string)$r['_waiter_name']) ?></td><td><?= e((string)($r['phone']??'')) ?></td><td style="min-width:220px;white-space:pre-wrap"><?= e(trim((string)($r['notes']??'')."\n".(string)($r['internal_note']??''))) ?></td><td><a class="btn btn-small btn-light" href="/admin/reservation.php?id=<?= (int)$r['id'] ?>">Otwórz</a></td></tr><?php endforeach; ?>
<?php if (!$rows): ?><tr><td colspan="9">Brak rezerwacji na wybrany dzień.</td></tr><?php endif; ?></tbody></table></div>
<p class="muted" style="margin-top:14px">Uwzględniono nowe, potwierdzone i zrealizowane rezerwacje. Wyłączono odrzucone, anulowane i no-show. Status „Nowa” wymaga decyzji obsługi. Dokument zawiera dane osobowe.</p>
</section>
<?php render_admin_footer(); ?>