Files
parkerslingshotrentals/admin/index.php
T
myron 85448d18c5 Add digital e-signature waiver + update How It Works to 5 steps
- waiver.php: full rental agreement with canvas e-signature pad, 6 required
  checkboxes, typed name field; stores sig image + IP + timestamp in DB;
  emails signed confirmation to customer and admin
- bookings table: add waiver_signed, waiver_signed_at, waiver_ip, waiver_name,
  waiver_sig columns
- contact.php: confirmation email now includes Sign Rental Agreement button/link
- admin/index.php: Waiver column shows Signed (date) or Pending + Send Link
- index.html: How It Works expanded to 5 steps (added Get Approved + Sign Waiver
  before Hit the Road); insurance updated to Proof of insurance required; FAQ
  and JSON-LD updated to match

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 13:49:31 +00:00

324 lines
16 KiB
PHP

<?php
require_once __DIR__ . '/db.php';
session_start();
$isAjax = !empty($_SERVER['HTTP_X_REQUESTED_WITH']) || (($_SERVER['HTTP_ACCEPT'] ?? '') === 'application/json');
// ── Auth ──────────────────────────────────────────────────────────────────────
if ($_POST['action'] ?? '' === 'login') {
if ($_POST['username'] === ADMIN_USER && password_verify($_POST['password'] ?? '', ADMIN_PASS)) {
$_SESSION[ADMIN_SESSION_KEY] = true;
}
header('Location: /admin/'); exit;
}
if ($_GET['action'] ?? '' === 'logout') {
session_destroy(); header('Location: /admin/'); exit;
}
$authed = !empty($_SESSION[ADMIN_SESSION_KEY]);
// ── AJAX handlers (must be authenticated) ────────────────────────────────────
if ($isAjax && $authed) {
header('Content-Type: application/json');
$action = $_POST['action'] ?? $_GET['action'] ?? '';
if ($action === 'update_status') {
$id = (int)($_POST['id'] ?? 0);
$status = $_POST['status'] ?? '';
$allowed = ['pending','confirmed','completed','cancelled'];
if ($id && in_array($status, $allowed)) {
db()->prepare("UPDATE bookings SET status=? WHERE id=?")->execute([$status, $id]);
echo json_encode(['ok'=>true]);
} else {
echo json_encode(['error'=>'Invalid']);
}
exit;
}
if ($action === 'save_admin_notes') {
$id = (int)($_POST['id'] ?? 0);
$notes = substr(trim($_POST['notes'] ?? ''), 0, 1000);
db()->prepare("UPDATE bookings SET admin_notes=? WHERE id=?")->execute([$notes, $id]);
echo json_encode(['ok'=>true]);
exit;
}
if ($action === 'block_date') {
$date = $_POST['date'] ?? '';
$reason = substr($_POST['reason'] ?? '', 0, 200);
if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $date)) {
db()->prepare("INSERT IGNORE INTO blocked_dates (block_date, reason) VALUES (?,?)")->execute([$date, $reason]);
echo json_encode(['ok'=>true]);
} else { echo json_encode(['error'=>'Invalid date']); }
exit;
}
if ($action === 'unblock_date') {
$id = (int)($_POST['id'] ?? 0);
db()->prepare("DELETE FROM blocked_dates WHERE id=?")->execute([$id]);
echo json_encode(['ok'=>true]);
exit;
}
exit;
}
// ── Login page ────────────────────────────────────────────────────────────────
if (!$authed) { ?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>Admin Login — Parker County Slingshot Rentals</title>
<style>
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:system-ui,sans-serif;background:#0d0d0d;display:flex;align-items:center;justify-content:center;min-height:100vh}
.box{background:#1a1a1a;border:1px solid rgba(255,255,255,.1);border-radius:12px;padding:2.5rem;width:100%;max-width:360px}
h1{color:#f97316;font-size:1.4rem;margin-bottom:.25rem}
p{color:rgba(255,255,255,.4);font-size:.85rem;margin-bottom:2rem}
label{display:block;color:rgba(255,255,255,.6);font-size:.85rem;margin-bottom:.4rem}
input{width:100%;background:rgba(255,255,255,.06);border:1px solid rgba(255,255,255,.12);border-radius:8px;padding:.75rem 1rem;color:#fff;font-size:.95rem;margin-bottom:1.25rem;outline:none}
input:focus{border-color:#f97316}
button{width:100%;background:#f97316;color:#fff;border:none;border-radius:8px;padding:.85rem;font-size:1rem;font-weight:700;cursor:pointer}
button:hover{background:#ea580c}
</style>
</head>
<body>
<div class="box">
<h1>Parker Admin</h1>
<p>Slingshot Rentals Management</p>
<form method="POST">
<input type="hidden" name="action" value="login">
<label>Username</label>
<input type="text" name="username" required autofocus>
<label>Password</label>
<input type="password" name="password" required>
<button type="submit">Sign In</button>
</form>
</div>
</body>
</html>
<?php exit; }
// ── Dashboard data ────────────────────────────────────────────────────────────
$statusFilter = $_GET['status'] ?? '';
$where = $statusFilter ? "WHERE status = " . db()->quote($statusFilter) : '';
$bookings = db()->query("SELECT * FROM bookings {$where} ORDER BY rental_date ASC, created_at DESC")->fetchAll();
$blocked = db()->query("SELECT * FROM blocked_dates ORDER BY block_date ASC")->fetchAll();
$stats = db()->query("
SELECT
COUNT(*) AS total,
SUM(status='pending') AS pending,
SUM(status='confirmed') AS confirmed,
SUM(status='completed') AS completed,
SUM(status='cancelled') AS cancelled,
SUM(CASE WHEN status IN ('confirmed','completed') THEN amount ELSE 0 END) AS revenue
FROM bookings
")->fetch();
$statusColors = ['pending'=>'#d97706','confirmed'=>'#16a34a','completed'=>'#2563eb','cancelled'=>'#dc2626'];
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>Admin — Parker County Slingshot Rentals</title>
<style>
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:system-ui,sans-serif;background:#f3f4f6;color:#111;min-height:100vh}
header{background:#0d0d0d;color:#fff;padding:1rem 2rem;display:flex;align-items:center;justify-content:space-between}
header h1{color:#f97316;font-size:1.2rem;font-weight:800}
header a{color:rgba(255,255,255,.5);font-size:.85rem;text-decoration:none}
header a:hover{color:#fff}
.main{max-width:1200px;margin:0 auto;padding:2rem 1.5rem}
.stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:1rem;margin-bottom:2rem}
.stat{background:#fff;border-radius:10px;padding:1.25rem;border:1px solid #e5e7eb;text-align:center}
.stat-val{font-size:2rem;font-weight:800;line-height:1}
.stat-lbl{font-size:.8rem;color:#6b7280;margin-top:.25rem;text-transform:uppercase;letter-spacing:.5px}
.card{background:#fff;border-radius:10px;border:1px solid #e5e7eb;margin-bottom:2rem}
.card-header{padding:1rem 1.5rem;border-bottom:1px solid #f3f4f6;display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:.75rem}
.card-header h2{font-size:1rem;font-weight:700}
.filters a{padding:.35rem .75rem;border-radius:6px;font-size:.8rem;text-decoration:none;color:#6b7280;border:1px solid #e5e7eb;margin-left:.35rem}
.filters a.active,.filters a:hover{background:#f97316;color:#fff;border-color:#f97316}
table{width:100%;border-collapse:collapse;font-size:.875rem}
th{background:#f9fafb;padding:.6rem 1rem;text-align:left;font-weight:600;color:#6b7280;font-size:.75rem;text-transform:uppercase;letter-spacing:.5px;border-bottom:1px solid #e5e7eb}
td{padding:.75rem 1rem;border-bottom:1px solid #f3f4f6;vertical-align:top}
tr:last-child td{border-bottom:none}
tr:hover td{background:#fafafa}
.badge{display:inline-block;padding:.2rem .6rem;border-radius:999px;font-size:.72rem;font-weight:700;text-transform:uppercase;letter-spacing:.5px}
.badge-pending{background:#fef3c7;color:#92400e}
.badge-confirmed{background:#dcfce7;color:#14532d}
.badge-completed{background:#dbeafe;color:#1e3a8a}
.badge-cancelled{background:#fee2e2;color:#7f1d1d}
select.status-sel{font-size:.8rem;border:1px solid #e5e7eb;border-radius:6px;padding:.25rem .5rem;cursor:pointer}
textarea.notes-ta{width:100%;font-size:.8rem;border:1px solid #e5e7eb;border-radius:6px;padding:.4rem .6rem;resize:vertical;min-height:50px;font-family:inherit}
.save-btn{font-size:.75rem;background:#f97316;color:#fff;border:none;border-radius:5px;padding:.25rem .6rem;cursor:pointer;margin-top:.25rem}
.block-form{display:flex;gap:.75rem;flex-wrap:wrap;align-items:flex-end;padding:1rem 1.5rem}
.block-form input{border:1px solid #e5e7eb;border-radius:6px;padding:.45rem .75rem;font-size:.875rem}
.block-form button{background:#dc2626;color:#fff;border:none;border-radius:6px;padding:.45rem 1rem;font-size:.875rem;font-weight:600;cursor:pointer}
.block-list{padding:0 1.5rem 1rem}
.block-item{display:flex;align-items:center;gap:.75rem;padding:.5rem 0;border-bottom:1px solid #f3f4f6;font-size:.875rem}
.block-item:last-child{border-bottom:none}
.del-btn{background:none;border:none;color:#dc2626;cursor:pointer;font-size:1rem;padding:0}
@media(max-width:700px){.main{padding:1rem}.stat-val{font-size:1.5rem}}
</style>
</head>
<body>
<header>
<h1>Parker County Slingshot — Admin</h1>
<a href="?action=logout">Sign Out</a>
</header>
<div class="main">
<!-- Stats -->
<div class="stats">
<div class="stat"><div class="stat-val"><?= $stats['total'] ?></div><div class="stat-lbl">Total Bookings</div></div>
<div class="stat"><div class="stat-val" style="color:#d97706"><?= $stats['pending'] ?></div><div class="stat-lbl">Pending</div></div>
<div class="stat"><div class="stat-val" style="color:#16a34a"><?= $stats['confirmed'] ?></div><div class="stat-lbl">Confirmed</div></div>
<div class="stat"><div class="stat-val" style="color:#2563eb"><?= $stats['completed'] ?></div><div class="stat-lbl">Completed</div></div>
<div class="stat"><div class="stat-val" style="color:#f97316">$<?= number_format($stats['revenue'],0) ?></div><div class="stat-lbl">Revenue</div></div>
</div>
<!-- Bookings -->
<div class="card">
<div class="card-header">
<h2>Bookings</h2>
<div class="filters">
<a href="/admin/" class="<?= !$statusFilter ? 'active':'' ?>">All</a>
<a href="/admin/?status=pending" class="<?= $statusFilter==='pending' ? 'active':'' ?>">Pending</a>
<a href="/admin/?status=confirmed" class="<?= $statusFilter==='confirmed' ? 'active':'' ?>">Confirmed</a>
<a href="/admin/?status=completed" class="<?= $statusFilter==='completed' ? 'active':'' ?>">Completed</a>
<a href="/admin/?status=cancelled" class="<?= $statusFilter==='cancelled' ? 'active':'' ?>">Cancelled</a>
</div>
</div>
<?php if (empty($bookings)): ?>
<div style="padding:3rem;text-align:center;color:#9ca3af">No bookings found.</div>
<?php else: ?>
<div style="overflow-x:auto">
<table>
<thead>
<tr>
<th>Ref</th><th>Customer</th><th>Package</th><th>Date</th>
<th>Amount</th><th>Status</th><th>Waiver</th><th>Admin Notes</th><th>Received</th>
</tr>
</thead>
<tbody>
<?php foreach ($bookings as $b): ?>
<tr>
<td><strong><?= htmlspecialchars($b['booking_ref']) ?></strong></td>
<td>
<strong><?= htmlspecialchars($b['name']) ?></strong><br>
<a href="mailto:<?= htmlspecialchars($b['email']) ?>" style="color:#f97316;font-size:.8rem"><?= htmlspecialchars($b['email']) ?></a>
<?php if ($b['phone']): ?><br><span style="font-size:.8rem;color:#6b7280"><?= htmlspecialchars($b['phone']) ?></span><?php endif; ?>
</td>
<td>
<?php $pkg = PACKAGES[$b['package']] ?? ['label'=>$b['package']]; ?>
<?= htmlspecialchars($pkg['label']) ?>
</td>
<td>
<strong><?= date('M j, Y', strtotime($b['rental_date'])) ?></strong>
<?php if ($b['end_date'] !== $b['rental_date']): ?>
<br><span style="font-size:.8rem;color:#6b7280">→ <?= date('M j', strtotime($b['end_date'])) ?></span>
<?php endif; ?>
</td>
<td>$<?= number_format($b['amount'],2) ?></td>
<td>
<select class="status-sel" data-id="<?= $b['id'] ?>" onchange="updateStatus(this)">
<?php foreach (['pending','confirmed','completed','cancelled'] as $s): ?>
<option value="<?= $s ?>" <?= $b['status']===$s?'selected':'' ?>><?= ucfirst($s) ?></option>
<?php endforeach; ?>
</select>
</td>
<td>
<?php if ($b['waiver_signed']): ?>
<span class="badge" style="background:#dcfce7;color:#14532d">&#10003; Signed</span>
<br><span style="font-size:.72rem;color:#9ca3af"><?= date('M j g:ia', strtotime($b['waiver_signed_at'])) ?></span>
<?php else: ?>
<a href="https://parkerslingshotrentals.com/waiver.php?ref=<?= urlencode($b['booking_ref']) ?>" target="_blank"
style="font-size:.78rem;color:#f97316;text-decoration:none">Send Link &#8599;</a>
<br><span class="badge" style="background:#fef3c7;color:#92400e;margin-top:.25rem">Pending</span>
<?php endif; ?>
</td>
<td>
<textarea class="notes-ta" data-id="<?= $b['id'] ?>"><?= htmlspecialchars($b['admin_notes'] ?? '') ?></textarea>
<button class="save-btn" onclick="saveNotes(this)">Save</button>
</td>
<td style="font-size:.8rem;color:#9ca3af;white-space:nowrap"><?= date('M j g:ia', strtotime($b['created_at'])) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</div>
<!-- Block Dates -->
<div class="card">
<div class="card-header"><h2>Block Dates (maintenance / personal use)</h2></div>
<form class="block-form" onsubmit="blockDate(event)">
<div>
<label style="font-size:.8rem;color:#6b7280;display:block;margin-bottom:.25rem">Date</label>
<input type="date" id="blockDate" required min="<?= date('Y-m-d') ?>">
</div>
<div style="flex:1;min-width:180px">
<label style="font-size:.8rem;color:#6b7280;display:block;margin-bottom:.25rem">Reason (optional)</label>
<input type="text" id="blockReason" placeholder="e.g. Maintenance">
</div>
<button type="submit">Block Date</button>
</form>
<div class="block-list">
<?php if (empty($blocked)): ?>
<p style="color:#9ca3af;font-size:.875rem;padding:.5rem 0">No dates blocked.</p>
<?php else: ?>
<?php foreach ($blocked as $bl): ?>
<div class="block-item" id="blocked-<?= $bl['id'] ?>">
<button class="del-btn" onclick="unblockDate(<?= $bl['id'] ?>)" title="Remove">✕</button>
<strong><?= date('M j, Y', strtotime($bl['block_date'])) ?></strong>
<?php if ($bl['reason']): ?><span style="color:#6b7280">— <?= htmlspecialchars($bl['reason']) ?></span><?php endif; ?>
</div>
<?php endforeach; ?>
<?php endif; ?>
</div>
</div>
</div>
<script>
function updateStatus(sel) {
fetch('/admin/', {
method:'POST',
headers:{'Content-Type':'application/x-www-form-urlencoded','X-Requested-With':'XMLHttpRequest'},
body:'action=update_status&id='+sel.dataset.id+'&status='+sel.value
}).then(r=>r.json()).then(d=>{ if(!d.ok) alert('Error saving status'); });
}
function saveNotes(btn) {
var ta = btn.previousElementSibling;
fetch('/admin/', {
method:'POST',
headers:{'Content-Type':'application/x-www-form-urlencoded','X-Requested-With':'XMLHttpRequest'},
body:'action=save_admin_notes&id='+ta.dataset.id+'&notes='+encodeURIComponent(ta.value)
}).then(r=>r.json()).then(d=>{
btn.textContent = d.ok ? 'Saved ✓' : 'Error';
setTimeout(()=>btn.textContent='Save', 1500);
});
}
function blockDate(e) {
e.preventDefault();
var date = document.getElementById('blockDate').value;
var reason = document.getElementById('blockReason').value;
fetch('/admin/', {
method:'POST',
headers:{'Content-Type':'application/x-www-form-urlencoded','X-Requested-With':'XMLHttpRequest'},
body:'action=block_date&date='+date+'&reason='+encodeURIComponent(reason)
}).then(r=>r.json()).then(d=>{ if(d.ok) location.reload(); else alert(d.error); });
}
function unblockDate(id) {
if (!confirm('Remove this blocked date?')) return;
fetch('/admin/', {
method:'POST',
headers:{'Content-Type':'application/x-www-form-urlencoded','X-Requested-With':'XMLHttpRequest'},
body:'action=unblock_date&id='+id
}).then(r=>r.json()).then(d=>{ if(d.ok) document.getElementById('blocked-'+id).remove(); });
}
</script>
</body>
</html>