| 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();
ensure_reservations_schema();
$currentUser = admin_user();
$isAdmin = (($currentUser['role'] ?? '') === 'admin');
// Masowe porządkowanie – tylko administrator, CSRF i bez automatycznych e-maili.
// Fizyczne usuwanie kasuje również historię reservation_events (FK CASCADE).
if (($_SERVER['REQUEST_METHOD'] ?? 'GET') === 'POST') {
if (!$isAdmin) { http_response_code(403); exit('Brak uprawnień administratora.'); }
verify_csrf();
$action = (string)($_POST['bulk_action'] ?? '');
$ids = $_POST['ids'] ?? [];
if (!is_array($ids)) $ids = [];
$ids = array_values(array_unique(array_filter(array_map('intval', array_slice($ids, 0, 200)), static fn(int $id): bool => $id > 0)));
if (!$ids) {
flash('error', 'Najpierw zaznacz rezerwacje.');
redirect('/admin/reservations.php');
}
if (!in_array($action, ['cancel', 'delete'], true)) {
flash('error', 'Nieznana operacja.');
redirect('/admin/reservations.php');
}
$pdo = db();
try {
$placeholders = implode(',', array_fill(0, count($ids), '?'));
$pdo->beginTransaction();
if ($action === 'cancel') {
$selected = $pdo->prepare('SELECT id FROM reservations WHERE id IN (' . $placeholders . ') AND status NOT IN ("cancelled", "completed") FOR UPDATE');
$selected->execute($ids);
$changedIds = array_map('intval', $selected->fetchAll(PDO::FETCH_COLUMN));
$stmt = $pdo->prepare('UPDATE reservations SET status = "cancelled", cancelled_at = NOW() WHERE id IN (' . $placeholders . ') AND status NOT IN ("cancelled", "completed")');
$stmt->execute($ids);
$affected = $stmt->rowCount();
$event = $pdo->prepare('INSERT INTO reservation_events (reservation_id, event_type, message, admin_user_id) VALUES (?, "status", ?, ?)');
foreach ($changedIds as $changedId) {
$event->execute([$changedId, 'Anulowano zbiorczo z panelu bez wiadomości e-mail.', (int)$currentUser['id']]);
}
} else {
if ((string)($_POST['confirmed_delete'] ?? '') !== 'TAK') {
throw new RuntimeException('Usuwanie wymaga dodatkowego potwierdzenia.');
}
$stmt = $pdo->prepare('DELETE FROM reservations WHERE id IN (' . $placeholders . ')');
$stmt->execute($ids);
$affected = $stmt->rowCount();
}
admin_log('reservations_bulk_' . $action,
'Operacja ' . $action . ' na ID: ' . implode(',', $ids) . '; zmieniono: ' . $affected);
$pdo->commit();
flash('success', ($action === 'cancel' ? 'Anulowano' : 'Usunięto') . ' rezerwacje: ' . $affected . '.');
} catch (Throwable $e) {
if ($pdo->inTransaction()) $pdo->rollBack();
error_log('[POLANA RESERVATION BULK] ' . $e->getMessage());
flash('error', 'Nie udało się wykonać operacji.');
}
redirect('/admin/reservations.php');
}
$status = trim((string)($_GET['status'] ?? ''));
$q = trim((string)($_GET['q'] ?? ''));
$dateFrom = trim((string)($_GET['from'] ?? ''));
$dateTo = trim((string)($_GET['to'] ?? ''));
$where = [];
$params = [];
if ($status !== '' && array_key_exists($status, reservation_statuses())) {
$where[] = 'status = ?';
$params[] = $status;
}
if ($q !== '') {
// POLANA_V1813_SEARCH_ALIAS: znajdz tez stary rekord po widocznym numerze #POLANA-XXXX.
$searchCode = ltrim($q, '#');
$like = '%' . $searchCode . '%';
if (preg_match('/^POLANA-(\d+)$/iD', $searchCode, $m) && (int)$m[1] > 0) {
$where[] = '(reservation_code LIKE ? OR guest_name LIKE ? OR phone LIKE ? OR email LIKE ? OR id = ?)';
array_push($params, $like, $like, $like, $like, (int)$m[1]);
} else {
$where[] = '(reservation_code LIKE ? OR guest_name LIKE ? OR phone LIKE ? OR email LIKE ?)';
array_push($params, $like, $like, $like, $like);
}
}
if ($dateFrom !== '') {
$where[] = 'reservation_date >= ?';
$params[] = $dateFrom;
}
if ($dateTo !== '') {
$where[] = 'reservation_date <= ?';
$params[] = $dateTo;
}
$whereSql = $where ? (' WHERE ' . implode(' AND ', $where)) : '';
$countStmt = db()->prepare('SELECT COUNT(*) FROM reservations' . $whereSql);
$countStmt->execute($params);
$filteredTotal = (int)$countStmt->fetchColumn();
$perPage = 40;
$totalPages = max(1, (int)ceil($filteredTotal / $perPage));
$page = max(1, min($totalPages, (int)($_GET['page'] ?? 1)));
$offset = ($page - 1) * $perPage;
$sql = 'SELECT * FROM reservations' . $whereSql
. ' ORDER BY reservation_date ASC, reservation_time ASC, id DESC'
. ' LIMIT ' . $perPage . ' OFFSET ' . $offset;
$stmt = db()->prepare($sql);
$stmt->execute($params);
$reservations = $stmt->fetchAll();
// Podświetl potencjalne duplikaty w obrębie bieżącej strony listy.
$groups = [];
foreach ($reservations as $reservation) {
if (in_array((string)$reservation['status'], ['cancelled', 'rejected'], true)) continue;
foreach (reservation_contact_keys($reservation) as $contact) {
$key = $reservation['reservation_date'] . '|' . substr((string)$reservation['reservation_time'], 0, 5) . '|' . $contact;
$groups[$key][] = (int)$reservation['id'];
}
}
$duplicates = [];
foreach ($groups as $rowIds) {
if (count($rowIds) > 1) foreach ($rowIds as $id) $duplicates[$id] = true;
}
$paginationUrl = static function (int $target) use ($q, $status, $dateFrom, $dateTo): string {
return '/admin/reservations.php?' . http_build_query([
'q' => $q, 'status' => $status, 'from' => $dateFrom, 'to' => $dateTo, 'page' => $target,
]);
};
$newCount = (int)db()->query("SELECT COUNT(*) FROM reservations WHERE status='new'")->fetchColumn();
$todayCount = (int)db()->query("SELECT COUNT(*) FROM reservations WHERE reservation_date = CURDATE() AND status NOT IN ('rejected','cancelled')")->fetchColumn();
$upcomingCount = (int)db()->query("SELECT COUNT(*) FROM reservations WHERE reservation_date >= CURDATE() AND status='confirmed'")->fetchColumn();
$isWaiterView = ($currentUser['role'] ?? '') === 'waiter';
render_admin_header('Rezerwacje');
?>
<div class="admin-title-row">
<div>
<h1>Rezerwacje</h1>
<p><?= $isWaiterView ? 'Podgląd bieżących i przyszłych rezerwacji.' : 'Prośby z formularza online i odpowiedzi do gości.' ?></p>
</div>
<div class="actions">
<a class="btn btn-light" href="/rezerwacja" target="_blank" rel="noopener">Formularz ↗</a>
<a class="btn btn-primary" href="/admin/reservations-day.php?date=<?= e(date('Y-m-d')) ?>">Plan sali / Excel / PDF</a>
<a class="btn btn-primary" href="/admin/reservation-settings.php">SMTP / ustawienia</a>
<?php if ($isAdmin): ?>
<a class="btn btn-light" href="/admin/reservation-integrations.php">Kalendarz Google / ntfy</a>
<a class="btn btn-light" href="/admin/database-backup.php">Backup SQL</a>
<?php endif; ?>
</div>
</div>
<div class="grid-cards reservation-stats">
<div class="card">
<h3>Nowe</h3>
<div class="stat"><?= $newCount ?></div>
</div>
<div class="card">
<h3>Dzisiaj</h3>
<div class="stat"><?= $todayCount ?></div>
</div>
<div class="card">
<h3>Potwierdzone nadchodzące</h3>
<div class="stat"><?= $upcomingCount ?></div>
</div>
</div>
<form class="form-card reservation-filters" method="get">
<div class="reservation-filter-grid">
<label>
Szukaj
<input name="q" value="<?= e($q) ?>" placeholder="kod, nazwisko, telefon, e-mail">
</label>
<label>
Status
<select name="status">
<option value="">Wszystkie</option>
<?php foreach (reservation_statuses() as $key => $label): ?>
<option value="<?= e($key) ?>" <?= $status === $key ? 'selected' : '' ?>><?= e($label) ?></option>
<?php endforeach; ?>
</select>
</label>
<label>
Od
<input type="date" name="from" value="<?= e($dateFrom) ?>">
</label>
<label>
Do
<input type="date" name="to" value="<?= e($dateTo) ?>">
</label>
</div>
<div class="actions">
<button class="btn btn-primary" type="submit">Filtruj</button>
<a class="btn btn-light" href="/admin/reservations.php">Wyczyść</a>
</div>
</form>
<div class="form-card" style="margin:18px 0;padding:14px 18px">
<form method="get" action="/admin/reservations-day.php" style="display:flex;align-items:end;gap:12px;flex-wrap:wrap">
<label style="min-width:180px">Lista rezerwacji na dzień<br><input type="date" name="date" value="<?= e(($dateFrom !== '' && $dateFrom === $dateTo) ? $dateFrom : date('Y-m-d')) ?>" onchange="this.form.requestSubmit ? this.form.requestSubmit() : this.form.submit()" required></label>
<noscript><button type="submit" class="btn btn-primary">Zobacz plan sali</button></noscript>
<a class="btn btn-primary" href="/admin/reservations-day.php?date=<?= e(date('Y-m-d')) ?>">Otwórz kalendarz</a>
<span class="muted">Na raporcie są przyciski Excel i PDF / druk.</span>
</form>
</div>
<p class="muted">Wyników: <?= $filteredTotal ?> · strona <?= $page ?> z <?= $totalPages ?>. <?= count($duplicates) ? 'Uwaga: zaznaczono potencjalne duplikaty na tej stronie.' : '' ?></p>
<?php if ($isAdmin): ?>
<form id="reservation-bulk" method="post">
<?= csrf_field() ?>
<input type="hidden" name="confirmed_delete" value="">
<div class="actions" style="margin:12px 0">
<label><input type="checkbox" id="select-all-reservations"> Zaznacz widoczne</label>
<select name="bulk_action" required style="max-width:230px">
<option value="">Działanie dla zaznaczonych</option>
<option value="cancel">Anuluj bez wysyłania e-maila</option>
<option value="delete">Usuń trwale (spam)</option>
</select>
<button type="submit" class="btn btn-light">Wykonaj</button>
</div>
<?php endif; ?>
<div class="table-wrap">
<table class="admin-table reservations-table">
<thead>
<tr>
<?php if ($isAdmin): ?><th><span class="sr-only">Zaznacz</span></th><?php endif; ?>
<th>Termin</th>
<th>Gość</th>
<th>Osoby</th>
<th>Kontakt</th>
<th>Status</th>
<th>Kod</th>
<th></th>
</tr>
</thead>
<tbody>
<?php if (!$reservations): ?>
<tr>
<td colspan="<?= $isAdmin ? 8 : 7 ?>" class="muted">Brak rezerwacji dla wybranych filtrów.</td>
</tr>
<?php endif; ?>
<?php foreach ($reservations as $reservation): ?>
<tr>
<?php if ($isAdmin): ?><td><input type="checkbox" name="ids[]" value="<?= (int)$reservation['id'] ?>" aria-label="Wybierz rezerwację <?= e(reservation_display_code((string)$reservation['reservation_code'])) ?>" form="reservation-bulk"></td><?php endif; ?>
<td>
<strong><?= e((new DateTimeImmutable($reservation['reservation_date']))->format('d.m.Y')) ?></strong><br>
<?= e(substr((string)$reservation['reservation_time'], 0, 5)) ?>
</td>
<td><?= e($reservation['guest_name']) ?></td>
<td><?= (int)$reservation['guests'] ?></td>
<td>
<?= e($reservation['phone']) ?><br>
<span class="muted"><?= e($reservation['email']) ?></span>
</td>
<td>
<span class="reservation-status reservation-status-<?= e($reservation['status']) ?>">
<?= e(reservation_status_label($reservation['status'])) ?>
</span>
<?php if (!empty($duplicates[(int)$reservation['id']])): ?>
<span style="display:block;margin-top:4px;font-size:11px;font-weight:bold;color:#a65a00">Możliwy duplikat</span>
<?php endif; ?>
</td>
<td><?= e(reservation_display_code((string)$reservation['reservation_code'])) ?></td>
<td>
<a class="btn btn-small btn-light" href="/admin/reservation.php?id=<?= (int)$reservation['id'] ?>">
Otwórz
</a>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php if ($isAdmin): ?></form><?php endif; ?>
<?php if ($totalPages > 1): ?>
<div class="actions" style="margin:15px 0;justify-content:center">
<?php if ($page > 1): ?><a class="btn btn-light" href="<?= e($paginationUrl($page-1)) ?>">← Poprzednia</a><?php endif; ?>
<span>Strona <?= $page ?> / <?= $totalPages ?></span>
<?php if ($page < $totalPages): ?><a class="btn btn-light" href="<?= e($paginationUrl($page+1)) ?>">Następna →</a><?php endif; ?>
</div>
<?php endif; ?>
<?php if ($isAdmin): ?>
<script>
(function () {
const all=document.getElementById('select-all-reservations');
if(!all) return;
all.addEventListener('change', function() {
document.querySelectorAll('input[name="ids[]"]').forEach(el => { el.checked = all.checked; });
});
const bulk = document.getElementById('reservation-bulk');
bulk.addEventListener('submit', function (event) {
const action = bulk.querySelector('[name="bulk_action"]').value;
const hasSelection = !!bulk.querySelector('input[name="ids[]"]:checked');
if (!action || !hasSelection) {
event.preventDefault();
alert('Wybierz działanie i zaznacz rezerwacje.');
return;
}
if (action === 'delete') {
if (!window.confirm('TRWAŁE USUNIĘCIE: wpisy i historia zostaną skasowane. Czy pobrałeś kopię SQL i na pewno chcesz kontynuować?')) {
event.preventDefault();
return;
}
bulk.querySelector('[name="confirmed_delete"]').value = 'TAK';
}
});
})();
</script>
<?php endif; ?>
<?php render_admin_footer(); ?>