mirror of
https://github.com/myronblair/parkerslingshotrentals
synced 2026-06-30 17:50:31 -05:00
85448d18c5
- 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>
324 lines
16 KiB
PHP
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">✓ 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 ↗</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+'¬es='+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>
|