Files
parkerslingshotrentals/admin/index.php
T
myron cca3129f6e Add Square deposit payment integration
- Square Web Payments SDK card element in booking form
- Delayed-capture hold ($100) on booking submit — not charged until confirmed
- Live payment status field: Verifying card → Authorizing → Confirmed w/ hold ID
- Admin: Capture / Void / Refund actions for each booking
- square_payment_id returned in API response for frontend confirmation display

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 18:33:16 +00:00

1000 lines
52 KiB
PHP

<?php
require_once dirname(__DIR__) . '/db.php';
// ── Cookie-based auth (no PHP sessions — avoids server caching/permission issues) ──
define('AUTH_COOKIE', 'parker_auth');
define('AUTH_SECRET', hash('sha256', ADMIN_PASS . ADMIN_SESSION_KEY)); // not reversible
function _authToken(): string {
$t = bin2hex(random_bytes(32));
$e = time() + 86400; // 24h
$s = hash_hmac('sha256', "$t|$e", AUTH_SECRET);
return "$t.$e.$s";
}
function _verifyAuth(): bool {
$c = $_COOKIE[AUTH_COOKIE] ?? '';
$p = explode('.', $c, 3);
if (count($p) !== 3) return false;
[$t, $e, $s] = $p;
if ((int)$e < time()) return false;
return hash_equals(hash_hmac('sha256', "$t|$e", AUTH_SECRET), $s);
}
function _setAuth(): void {
$secure = !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off';
setcookie(AUTH_COOKIE, _authToken(), [
'expires' => time() + 86400,
'path' => '/admin/',
'secure' => $secure,
'httponly' => true,
'samesite' => 'Lax',
]);
}
function _clearAuth(): void {
$secure = !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off';
setcookie(AUTH_COOKIE, '', ['expires' => time()-3600, 'path' => '/admin/', 'secure' => $secure, 'httponly' => true, 'samesite' => 'Lax']);
}
$isAjax = !empty($_SERVER['HTTP_X_REQUESTED_WITH']) || (($_SERVER['HTTP_ACCEPT'] ?? '') === 'application/json');
// ── Auth ──────────────────────────────────────────────────────────────────────
if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'login') {
if ($_POST['username'] === ADMIN_USER && password_verify($_POST['password'] ?? '', ADMIN_PASS)) {
_setAuth();
}
header('Location: /admin/'); exit;
}
if (($_GET['action'] ?? '') === 'logout') {
_clearAuth(); header('Location: /admin/'); exit;
}
$authed = _verifyAuth();
// ── AJAX handlers ─────────────────────────────────────────────────────────────
if ($isAjax && !$authed) {
http_response_code(401);
header('Content-Type: application/json');
echo json_encode(['error'=>'Session expired. Please reload and log in again.']);
exit;
}
if ($isAjax) {
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 === 'toggle_requirement') {
$id = (int)($_POST['id'] ?? 0);
$field = $_POST['field'] ?? '';
$allowed_fields = ['insurance_verified','deposit_received','license_verified'];
if ($id && in_array($field, $allowed_fields)) {
$stmt = db()->prepare("SELECT `{$field}` FROM bookings WHERE id=?");
$stmt->execute([$id]);
$current = (int)$stmt->fetchColumn();
$new = $current ? 0 : 1;
db()->prepare("UPDATE bookings SET `{$field}`=? WHERE id=?")->execute([$new, $id]);
echo json_encode(['ok'=>true,'value'=>$new]);
} else { echo json_encode(['error'=>'Invalid']); }
exit;
}
if ($action === 'send_reminder') {
$id = (int)($_POST['id'] ?? 0);
$keys = array_filter(explode(',', $_POST['items'] ?? ''));
$stmt = db()->prepare("SELECT * FROM bookings WHERE id=?");
$stmt->execute([$id]);
$b = $stmt->fetch();
if (!$b) { echo json_encode(['error'=>'Not found']); exit; }
$pkg = PACKAGES[$b['package']] ?? ['label' => $b['package']];
$dateLabel = date('F j, Y', strtotime($b['rental_date']));
$ref = $b['booking_ref'];
$itemDefs = [
'waiver' => [
'label' => 'Sign Your Rental Agreement',
'detail' => 'Your digital rental agreement still needs to be signed before your pickup. It only takes a minute and can be done on any device — no printer required.',
'cta' => "<div style='margin-top:10px'><a href='https://parkerslingshotrentals.com/waiver.php?ref={$ref}' style='display:inline-block;background:#f97316;color:#fff;text-decoration:none;padding:9px 20px;border-radius:6px;font-weight:700;font-size:13px'>Sign Agreement &rarr;</a></div>",
],
'insurance' => [
'label' => 'Proof of Personal Auto Insurance',
'detail' => 'You\'ll need to bring proof of valid personal auto insurance to pickup. A photo on your phone of your insurance card is fine. This is required before we can hand over the keys.',
'cta' => '',
],
'deposit' => [
'label' => 'Security Deposit',
'detail' => 'A refundable security deposit is required at the time of pickup. Please have it ready — cash or card accepted. It will be returned in full upon safe return of the vehicle.',
'cta' => '',
],
'license' => [
'label' => "Valid Driver's License",
'detail' => "Please bring your valid driver's license to pickup. We're required to verify it before you take the Slingshot out. Must match the name on the booking.",
'cta' => '',
],
];
$rowsHtml = '';
$n = 1;
foreach ($keys as $key) {
if (!isset($itemDefs[$key])) continue;
$d = $itemDefs[$key];
$rowsHtml .= "
<tr>
<td style='padding:14px 0;border-bottom:1px solid #f3f4f6;vertical-align:top;width:30px'>
<span style='display:inline-flex;align-items:center;justify-content:center;width:24px;height:24px;border-radius:50%;background:#fff7ed;border:2px solid #f97316;font-size:11px;font-weight:800;color:#f97316;flex-shrink:0'>{$n}</span>
</td>
<td style='padding:14px 0 14px 14px;border-bottom:1px solid #f3f4f6'>
<strong style='color:#111;font-size:14px'>" . htmlspecialchars($d['label']) . "</strong>
<p style='margin:5px 0 0;font-size:13px;color:#6b7280;line-height:1.5'>" . htmlspecialchars($d['detail']) . "</p>
{$d['cta']}
</td>
</tr>";
$n++;
}
if (!$rowsHtml) { echo json_encode(['error'=>'No items selected']); exit; }
$html = "
<div style='max-width:600px;margin:0 auto;font-family:Arial,sans-serif'>
<div style='background:#0d0d0d;padding:24px;text-align:center'>
<h1 style='color:#f97316;margin:0;font-size:20px'>Parker County Slingshot Rentals</h1>
</div>
<div style='padding:32px;background:#fff'>
<h2 style='margin-top:0;color:#111'>Almost Ready — A Few Things Before Pickup</h2>
<p style='color:#374151;margin-bottom:8px'>Hey " . htmlspecialchars($b['name']) . ", your <strong>" . htmlspecialchars($pkg['label']) . "</strong> rental on <strong>{$dateLabel}</strong> is coming up! (Ref: <strong>{$ref}</strong>)</p>
<p style='color:#374151;margin-bottom:20px'>To make sure pickup goes smoothly, here's what still needs to be taken care of:</p>
<table style='width:100%;border-collapse:collapse'>{$rowsHtml}</table>
<div style='margin-top:24px;padding:16px;background:#f9fafb;border-radius:8px'>
<p style='margin:0;font-size:13px;color:#6b7280'>Questions? Call or text <strong style='color:#111'>(817) 555-0199</strong> or reply to this email — we're happy to help.</p>
</div>
<p style='color:#374151;margin-top:24px'>Ride on,<br><strong>The Parker County Slingshot Team</strong></p>
</div>
<div style='background:#f3f4f6;padding:16px;text-align:center'>
<p style='margin:0;font-size:12px;color:#9ca3af'>&copy; " . date('Y') . " Parker County Slingshot Rentals &mdash; Weatherford, TX</p>
</div>
</div>";
$sent = sendEmail($b['email'], $b['name'], "Action Needed Before Your Rental — {$ref}", $html);
echo json_encode(['ok'=>true]);
exit;
}
if ($action === 'square_capture') {
$id = (int)($_POST['id'] ?? 0);
$stmt = db()->prepare("SELECT square_payment_id FROM bookings WHERE id=?");
$stmt->execute([$id]);
$b = $stmt->fetch();
$pid = $b['square_payment_id'] ?? '';
if (!$pid) { echo json_encode(['error'=>'No payment on file']); exit; }
$resp = squareApi('POST', "/payments/{$pid}/complete");
if (($resp['payment']['status'] ?? '') === 'COMPLETED') {
db()->prepare("UPDATE bookings SET square_payment_status='COMPLETED', deposit_paid=?, deposit_received=1 WHERE id=?")
->execute([DEPOSIT_AMOUNT, $id]);
echo json_encode(['ok'=>true,'status'=>'COMPLETED']);
} else {
echo json_encode(['error' => $resp['errors'][0]['detail'] ?? 'Capture failed']);
}
exit;
}
if ($action === 'square_void') {
$id = (int)($_POST['id'] ?? 0);
$stmt = db()->prepare("SELECT square_payment_id FROM bookings WHERE id=?");
$stmt->execute([$id]);
$b = $stmt->fetch();
$pid = $b['square_payment_id'] ?? '';
if (!$pid) { echo json_encode(['error'=>'No payment on file']); exit; }
$resp = squareApi('POST', "/payments/{$pid}/cancel");
if (($resp['payment']['status'] ?? '') === 'CANCELED') {
db()->prepare("UPDATE bookings SET square_payment_status='CANCELED' WHERE id=?")->execute([$id]);
echo json_encode(['ok'=>true,'status'=>'CANCELED']);
} else {
echo json_encode(['error' => $resp['errors'][0]['detail'] ?? 'Void failed']);
}
exit;
}
if ($action === 'square_refund') {
$id = (int)($_POST['id'] ?? 0);
$stmt = db()->prepare("SELECT square_payment_id, deposit_paid FROM bookings WHERE id=?");
$stmt->execute([$id]);
$b = $stmt->fetch();
$pid = $b['square_payment_id'] ?? '';
if (!$pid) { echo json_encode(['error'=>'No payment on file']); exit; }
$cents = (int)(((float)($b['deposit_paid'] ?: DEPOSIT_AMOUNT)) * 100);
$resp = squareApi('POST', '/refunds', [
'idempotency_key' => $pid . '-refund-' . time(),
'payment_id' => $pid,
'amount_money' => ['amount' => $cents, 'currency' => 'USD'],
'reason' => 'Security deposit refund — booking returned in good condition',
]);
if (!empty($resp['refund']['id'])) {
db()->prepare("UPDATE bookings SET square_payment_status='REFUNDED', square_refund_id=?, deposit_paid=0 WHERE id=?")
->execute([$resp['refund']['id'], $id]);
echo json_encode(['ok'=>true,'status'=>'REFUNDED']);
} else {
echo json_encode(['error' => $resp['errors'][0]['detail'] ?? 'Refund failed']);
}
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,
SUM(waiver_signed) AS waivers_signed,
SUM(insurance_verified) AS insurance_done,
SUM(deposit_received) AS deposits_done
FROM bookings
")->fetch();
?>
<!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;gap:1rem}
header h1{color:#f97316;font-size:1.15rem;font-weight:800;white-space:nowrap}
header a{color:rgba(255,255,255,.5);font-size:.85rem;text-decoration:none;white-space:nowrap}
header a:hover{color:#fff}
.main{max-width:1300px;margin:0 auto;padding:2rem 1.5rem}
/* Stats */
.stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(130px,1fr));gap:1rem;margin-bottom:2rem}
.stat{background:#fff;border-radius:10px;padding:1.1rem;border:1px solid #e5e7eb;text-align:center}
.stat-val{font-size:1.75rem;font-weight:800;line-height:1}
.stat-lbl{font-size:.72rem;color:#6b7280;margin-top:.3rem;text-transform:uppercase;letter-spacing:.5px}
/* Card */
.card{background:#fff;border-radius:10px;border:1px solid #e5e7eb;margin-bottom:2rem;overflow:hidden}
.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 */
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:.72rem;text-transform:uppercase;letter-spacing:.5px;border-bottom:1px solid #e5e7eb;white-space:nowrap}
td{padding:.7rem 1rem;border-bottom:1px solid #f3f4f6;vertical-align:middle}
.booking-row td{cursor:pointer}
.booking-row:hover td{background:#fafafa}
.detail-row td{padding:0;background:#f9fafb}
.detail-row.open td{border-bottom:3px solid #f97316}
/* Expand button */
.expand-btn{background:none;border:none;cursor:pointer;color:#9ca3af;font-size:1rem;padding:2px 6px;border-radius:4px;transition:transform .2s,color .2s;line-height:1}
.expand-btn.open{transform:rotate(90deg);color:#f97316}
/* Progress dots */
.dots{display:flex;gap:4px;align-items:center}
.dot{width:11px;height:11px;border-radius:50%;flex-shrink:0;transition:background .2s}
.dot-done{background:#22c55e}
.dot-skip{background:#d1d5db}
.dot-pending{background:#fbbf24}
/* Status badge / select */
.badge{display:inline-block;padding:.2rem .6rem;border-radius:999px;font-size:.72rem;font-weight:700;text-transform:uppercase;letter-spacing:.4px}
select.status-sel{font-size:.8rem;border:1px solid #e5e7eb;border-radius:6px;padding:.25rem .5rem;cursor:pointer;background:#fff}
/* ── Detail panel ───────────────────────────────────────── */
.detail-panel{display:grid;grid-template-columns:240px 1fr 1fr;gap:0;border-top:1px solid #e5e7eb}
@media(max-width:900px){.detail-panel{grid-template-columns:1fr}}
.dp-col{padding:1.5rem;border-right:1px solid #f3f4f6}
.dp-col:last-child{border-right:none}
.dp-col h3{font-size:.72rem;font-weight:700;text-transform:uppercase;letter-spacing:1px;color:#9ca3af;margin-bottom:1rem}
/* Contact info */
.ci-name{font-size:1rem;font-weight:700;margin-bottom:.2rem}
.ci-row{display:flex;gap:.5rem;align-items:baseline;font-size:.85rem;color:#6b7280;margin-bottom:.35rem}
.ci-row a{color:#f97316;text-decoration:none}
.ci-row a:hover{text-decoration:underline}
.ci-ref{display:inline-block;font-size:.75rem;font-weight:700;color:#f97316;background:#fff7ed;padding:.15rem .5rem;border-radius:4px;margin-bottom:.75rem}
.ci-field{font-size:.78rem;color:#9ca3af;text-transform:uppercase;letter-spacing:.5px;margin-bottom:.15rem;margin-top:.6rem}
/* Flow checklist */
.flow-list{display:flex;flex-direction:column;gap:.15rem}
.flow-step{display:flex;align-items:flex-start;gap:.75rem;padding:.65rem .5rem;border-radius:8px;transition:background .15s}
.flow-step:hover{background:rgba(0,0,0,.02)}
.flow-icon{width:26px;height:26px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:.75rem;font-weight:700;flex-shrink:0;margin-top:1px}
.flow-icon.done{background:#dcfce7;color:#16a34a}
.flow-icon.pending{background:#fef3c7;color:#d97706;font-size:.65rem}
.flow-icon.skip{background:#f3f4f6;color:#9ca3af}
.flow-body{flex:1;min-width:0}
.flow-label{font-size:.88rem;font-weight:600;color:#111;display:block}
.flow-label.muted{color:#9ca3af}
.flow-meta{font-size:.75rem;color:#9ca3af;margin-top:.1rem;display:block}
.flow-action{margin-top:.35rem}
.flow-toggle{background:none;border:1px solid #d1d5db;border-radius:5px;font-size:.73rem;padding:.2rem .6rem;cursor:pointer;color:#374151;transition:all .15s}
.flow-toggle:hover{border-color:#f97316;color:#f97316}
.flow-toggle.active{background:#dcfce7;border-color:#bbf7d0;color:#15803d}
.flow-link{font-size:.75rem;color:#f97316;text-decoration:none}
.flow-link:hover{text-decoration:underline}
/* Notes */
textarea.notes-ta{width:100%;font-size:.82rem;border:1px solid #e5e7eb;border-radius:6px;padding:.5rem .65rem;resize:vertical;min-height:72px;font-family:inherit;outline:none;transition:border-color .2s}
textarea.notes-ta:focus{border-color:#f97316}
.save-btn{margin-top:.5rem;font-size:.78rem;background:#f97316;color:#fff;border:none;border-radius:5px;padding:.3rem .75rem;cursor:pointer;transition:background .15s}
.save-btn:hover{background:#ea580c}
/* Reminder section */
.reminder-box{background:#fff7ed;border:1px solid #fed7aa;border-radius:8px;padding:1rem;margin-top:1.25rem}
.reminder-box h4{font-size:.78rem;font-weight:700;text-transform:uppercase;letter-spacing:.5px;color:#92400e;margin-bottom:.65rem}
.reminder-items{display:flex;flex-direction:column;gap:.4rem;margin-bottom:.85rem}
.reminder-items label{display:flex;align-items:flex-start;gap:.5rem;font-size:.83rem;color:#374151;cursor:pointer}
.reminder-items input[type=checkbox]{margin-top:2px;accent-color:#f97316;width:14px;height:14px}
.send-btn{width:100%;background:#f97316;color:#fff;border:none;border-radius:6px;padding:.55rem;font-size:.83rem;font-weight:700;cursor:pointer;transition:background .15s}
.send-btn:hover{background:#ea580c}
.send-btn:disabled{background:#d1d5db;cursor:not-allowed}
/* Block dates */
.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;line-height:1}
@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"><?= (int)$stats['total'] ?></div><div class="stat-lbl">Total Bookings</div></div>
<div class="stat"><div class="stat-val" style="color:#d97706"><?= (int)$stats['pending'] ?></div><div class="stat-lbl">Pending</div></div>
<div class="stat"><div class="stat-val" style="color:#16a34a"><?= (int)$stats['confirmed'] ?></div><div class="stat-lbl">Confirmed</div></div>
<div class="stat"><div class="stat-val" style="color:#2563eb"><?= (int)$stats['completed'] ?></div><div class="stat-lbl">Completed</div></div>
<div class="stat"><div class="stat-val" style="color:#f97316">$<?= number_format((float)$stats['revenue'],0) ?></div><div class="stat-lbl">Revenue</div></div>
<div class="stat"><div class="stat-val" style="color:#16a34a"><?= (int)$stats['waivers_signed'] ?></div><div class="stat-lbl">Waivers Signed</div></div>
<div class="stat"><div class="stat-val" style="color:#16a34a"><?= (int)$stats['insurance_done'] ?></div><div class="stat-lbl">Insurance OK</div></div>
<div class="stat"><div class="stat-val" style="color:#16a34a"><?= (int)$stats['deposits_done'] ?></div><div class="stat-lbl">Deposits Rcvd</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 style="width:36px"></th>
<th>Customer</th>
<th>Rental Date</th>
<th>Package</th>
<th>Amount</th>
<th>Status</th>
<th>Progress</th>
<th>Submitted</th>
</tr>
</thead>
<tbody>
<?php foreach ($bookings as $b):
$bid = $b['id'];
$pkg = PACKAGES[$b['package']] ?? ['label'=>$b['package']];
// Determine each step's state
$stepConfirmed = in_array($b['status'], ['confirmed','completed']);
$stepWaiver = (bool)$b['waiver_signed'];
$stepInsurance = (bool)$b['insurance_verified'];
$stepDeposit = (bool)$b['deposit_received'];
$stepLicense = (bool)$b['license_verified'];
// Dot colors: done=green, if cancelled skip all
$cancelled = $b['status'] === 'cancelled';
$dotClass = function($done) use ($cancelled) {
if ($cancelled) return 'dot-skip';
return $done ? 'dot-done' : 'dot-pending';
};
$allDone = $stepConfirmed && $stepWaiver && $stepInsurance && $stepDeposit && $stepLicense;
$pendingCount = ($cancelled ? 0 : (
(!$stepConfirmed?1:0)+(!$stepWaiver?1:0)+(!$stepInsurance?1:0)+(!$stepDeposit?1:0)+(!$stepLicense?1:0)
));
?>
<tr class="booking-row" onclick="toggleDetail(<?= $bid ?>)">
<td onclick="event.stopPropagation()">
<button class="expand-btn" id="expand-<?= $bid ?>" onclick="toggleDetail(<?= $bid ?>)">&#9658;</button>
</td>
<td>
<strong><?= htmlspecialchars($b['name']) ?></strong><br>
<span style="font-size:.78rem;color:#9ca3af"><?= htmlspecialchars($b['email']) ?></span>
</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:.75rem;color:#9ca3af">→ <?= date('M j', strtotime($b['end_date'])) ?></span>
<?php endif; ?>
</td>
<td style="font-size:.83rem"><?= htmlspecialchars($pkg['label']) ?></td>
<td style="font-weight:600">$<?= number_format($b['amount'],2) ?></td>
<td onclick="event.stopPropagation()">
<select class="status-sel" data-id="<?= $bid ?>" 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>
<div class="dots" title="Confirmed / Waiver / Insurance / Deposit / License">
<div class="dot <?= $dotClass($stepConfirmed) ?>" title="Confirmed"></div>
<div class="dot <?= $dotClass($stepWaiver) ?>" title="Waiver Signed"></div>
<div class="dot <?= $dotClass($stepInsurance) ?>" title="Insurance" id="dot-<?= $bid ?>-insurance_verified"></div>
<div class="dot <?= $dotClass($stepDeposit) ?>" title="Deposit" id="dot-<?= $bid ?>-deposit_received"></div>
<div class="dot <?= $dotClass($stepLicense) ?>" title="License" id="dot-<?= $bid ?>-license_verified"></div>
</div>
<?php if (!$cancelled && $pendingCount): ?>
<span style="font-size:.68rem;color:#d97706;display:block;margin-top:3px"><?= $pendingCount ?> pending</span>
<?php elseif ($allDone): ?>
<span style="font-size:.68rem;color:#16a34a;display:block;margin-top:3px">All done ✓</span>
<?php endif; ?>
</td>
<td style="font-size:.78rem;color:#9ca3af;white-space:nowrap"><?= date('M j g:ia', strtotime($b['created_at'])) ?></td>
</tr>
<!-- ── Detail Panel ───────────────────────────────────────────── -->
<tr class="detail-row" id="detail-<?= $bid ?>">
<td colspan="8">
<div class="detail-panel">
<!-- Column 1: Contact info -->
<div class="dp-col">
<h3>Customer</h3>
<div class="ci-ref"><?= htmlspecialchars($b['booking_ref']) ?></div>
<div class="ci-name"><?= htmlspecialchars($b['name']) ?></div>
<div class="ci-row"><a href="mailto:<?= htmlspecialchars($b['email']) ?>"><?= htmlspecialchars($b['email']) ?></a></div>
<?php if ($b['phone']): ?>
<div class="ci-row"><a href="tel:<?= htmlspecialchars($b['phone']) ?>"><?= htmlspecialchars($b['phone']) ?></a></div>
<?php endif; ?>
<div class="ci-field">Package</div>
<div style="font-size:.85rem;font-weight:600"><?= htmlspecialchars($pkg['label']) ?></div>
<div style="font-size:.82rem;color:#6b7280">$<?= number_format($b['amount'],2) ?></div>
<div class="ci-field">Rental Date</div>
<div style="font-size:.85rem;font-weight:600"><?= date('F j, Y', strtotime($b['rental_date'])) ?></div>
<?php if ($b['notes']): ?>
<div class="ci-field">Customer Message</div>
<div style="font-size:.82rem;color:#6b7280;font-style:italic"><?= nl2br(htmlspecialchars($b['notes'])) ?></div>
<?php endif; ?>
<div style="margin-top:1.25rem">
<div class="ci-field">Admin Notes</div>
<textarea class="notes-ta" id="notes-<?= $bid ?>"><?= htmlspecialchars($b['admin_notes'] ?? '') ?></textarea>
<button class="save-btn" onclick="saveNotes(<?= $bid ?>)">Save Notes</button>
</div>
</div>
<!-- Column 2: Booking flow checklist -->
<div class="dp-col">
<h3>Booking Flow</h3>
<div class="flow-list">
<!-- Step 1: Submitted -->
<div class="flow-step">
<div class="flow-icon done">✓</div>
<div class="flow-body">
<span class="flow-label">Booking Submitted</span>
<span class="flow-meta"><?= date('M j, Y g:ia', strtotime($b['created_at'])) ?></span>
</div>
</div>
<!-- Step 2: Confirmed -->
<div class="flow-step">
<div class="flow-icon <?= $stepConfirmed?'done':($cancelled?'skip':'pending') ?>">
<?= $stepConfirmed?'✓':($cancelled?'—':'2') ?>
</div>
<div class="flow-body">
<span class="flow-label <?= (!$stepConfirmed&&!$cancelled)?'':''; ?>">Booking Confirmed</span>
<span class="flow-meta">
<?php if ($stepConfirmed): ?>Confirmed — status: <?= ucfirst($b['status']) ?>
<?php elseif ($cancelled): ?>Cancelled
<?php else: ?>Awaiting confirmation — change status above
<?php endif; ?>
</span>
</div>
</div>
<!-- Step 3: Waiver -->
<div class="flow-step">
<div class="flow-icon <?= $stepWaiver?'done':($cancelled?'skip':'pending') ?>">
<?= $stepWaiver?'✓':($cancelled?'—':'3') ?>
</div>
<div class="flow-body">
<span class="flow-label">Rental Waiver Signed</span>
<span class="flow-meta">
<?php if ($stepWaiver): ?>
Signed by <?= htmlspecialchars($b['waiver_name'] ?? $b['name']) ?>
<?php if ($b['waiver_signed_at']): ?> on <?= date('M j g:ia', strtotime($b['waiver_signed_at'])) ?><?php endif; ?>
<?php elseif ($cancelled): ?>N/A
<?php else: ?>Not yet signed
<?php endif; ?>
</span>
<?php if (!$stepWaiver && !$cancelled): ?>
<div class="flow-action">
<a class="flow-link" href="https://parkerslingshotrentals.com/waiver.php?ref=<?= urlencode($b['booking_ref']) ?>" target="_blank">Open Waiver Link ↗</a>
</div>
<?php endif; ?>
</div>
</div>
<!-- Step 4: Insurance -->
<div class="flow-step">
<div class="flow-icon <?= $stepInsurance?'done':($cancelled?'skip':'pending') ?>" id="icon-<?= $bid ?>-insurance_verified">
<?= $stepInsurance?'✓':($cancelled?'—':'4') ?>
</div>
<div class="flow-body">
<span class="flow-label">Proof of Insurance Received</span>
<span class="flow-meta" id="meta-<?= $bid ?>-insurance_verified">
<?= $stepInsurance?'Verified — on file':($cancelled?'N/A':'Pending — verify at pickup') ?>
</span>
<?php if (!$cancelled): ?>
<div class="flow-action">
<button class="flow-toggle <?= $stepInsurance?'active':'' ?>"
id="btn-<?= $bid ?>-insurance_verified"
onclick="toggleReq(<?= $bid ?>,'insurance_verified',this)">
<?= $stepInsurance?'✓ Marked Received':'Mark Received' ?>
</button>
</div>
<?php endif; ?>
</div>
</div>
<!-- Step 5: Deposit (Square-aware) -->
<?php
$sqStatus = $b['square_payment_status'] ?? '';
$sqId = $b['square_payment_id'] ?? '';
$depositIcon = 'pending';
$depositLabel = '5';
if ($cancelled) { $depositIcon='skip'; $depositLabel='—'; }
elseif ($stepDeposit ||
$sqStatus==='COMPLETED') { $depositIcon='done'; $depositLabel='✓'; }
?>
<div class="flow-step">
<div class="flow-icon <?= $depositIcon ?>" id="icon-<?= $bid ?>-deposit_received">
<?= $depositLabel ?>
</div>
<div class="flow-body">
<span class="flow-label">Security Deposit — $<?= number_format(DEPOSIT_AMOUNT,0) ?></span>
<span class="flow-meta" id="meta-<?= $bid ?>-deposit_received">
<?php if ($cancelled): ?>N/A
<?php elseif ($sqStatus === 'COMPLETED'): ?>Captured — $<?= number_format((float)($b['deposit_paid']??DEPOSIT_AMOUNT),2) ?> charged
<?php elseif ($sqStatus === 'REFUNDED'): ?>Refunded — $<?= number_format((float)($b['deposit_paid']??DEPOSIT_AMOUNT),2) ?>
<?php elseif ($sqStatus === 'CANCELED'): ?>Hold voided — no charge
<?php elseif ($sqStatus === 'APPROVED' || $sqStatus === 'PENDING'): ?>Hold active — card authorized, not yet charged
<?php elseif ($stepDeposit): ?>Deposit marked received (manual)
<?php else: ?>Pending — no card on file yet
<?php endif; ?>
</span>
<?php if (!$cancelled): ?>
<div class="flow-action" id="deposit-actions-<?= $bid ?>">
<?php if ($sqStatus === 'APPROVED' || $sqStatus === 'PENDING'): ?>
<button class="flow-toggle" onclick="squareAction(<?= $bid ?>,'square_capture',this)" style="margin-right:4px">Capture $<?= number_format(DEPOSIT_AMOUNT,0) ?></button>
<button class="flow-toggle" onclick="squareAction(<?= $bid ?>,'square_void',this)" style="border-color:#dc2626;color:#dc2626">Void Hold</button>
<?php elseif ($sqStatus === 'COMPLETED'): ?>
<button class="flow-toggle" onclick="squareAction(<?= $bid ?>,'square_refund',this)" style="border-color:#2563eb;color:#2563eb">Refund $<?= number_format((float)($b['deposit_paid']??DEPOSIT_AMOUNT),2) ?></button>
<?php elseif (!$sqId): ?>
<button class="flow-toggle <?= $stepDeposit?'active':'' ?>"
id="btn-<?= $bid ?>-deposit_received"
onclick="toggleReq(<?= $bid ?>,'deposit_received',this)">
<?= $stepDeposit?'✓ Deposit Received':'Mark Deposit Received' ?>
</button>
<?php endif; ?>
</div>
<?php endif; ?>
</div>
</div>
<!-- Step 6: License -->
<div class="flow-step">
<div class="flow-icon <?= $stepLicense?'done':($cancelled?'skip':'pending') ?>" id="icon-<?= $bid ?>-license_verified">
<?= $stepLicense?'✓':($cancelled?'—':'6') ?>
</div>
<div class="flow-body">
<span class="flow-label">Driver's License Verified</span>
<span class="flow-meta" id="meta-<?= $bid ?>-license_verified">
<?= $stepLicense?'License verified':($cancelled?'N/A':'Verify at pickup') ?>
</span>
<?php if (!$cancelled): ?>
<div class="flow-action">
<button class="flow-toggle <?= $stepLicense?'active':'' ?>"
id="btn-<?= $bid ?>-license_verified"
onclick="toggleReq(<?= $bid ?>,'license_verified',this)">
<?= $stepLicense?'✓ License Verified':'Mark License Verified' ?>
</button>
</div>
<?php endif; ?>
</div>
</div>
</div><!-- /.flow-list -->
</div><!-- /.dp-col flow -->
<!-- Column 3: Reminder -->
<div class="dp-col">
<h3>Send Reminder Email</h3>
<p style="font-size:.82rem;color:#6b7280;margin-bottom:1rem">Select what the customer still needs to do, then send them a nudge email with clear instructions.</p>
<?php if ($cancelled): ?>
<p style="font-size:.82rem;color:#9ca3af">Not applicable for cancelled bookings.</p>
<?php else: ?>
<div class="reminder-box">
<h4>Include in Reminder</h4>
<div class="reminder-items" id="reminder-<?= $bid ?>">
<label>
<input type="checkbox" value="waiver" <?= !$stepWaiver?'checked':'' ?>>
Sign rental agreement
</label>
<label>
<input type="checkbox" value="insurance" <?= !$stepInsurance?'checked':'' ?>>
Bring proof of insurance
</label>
<label>
<input type="checkbox" value="deposit" <?= !$stepDeposit?'checked':'' ?>>
Security deposit info
</label>
<label>
<input type="checkbox" value="license" <?= !$stepLicense?'checked':'' ?>>
Bring driver's license
</label>
</div>
<button class="send-btn" id="remind-btn-<?= $bid ?>" onclick="sendReminder(<?= $bid ?>)">
Send Reminder Email
</button>
<div id="remind-status-<?= $bid ?>" style="margin-top:.6rem;font-size:.78rem;display:none"></div>
</div>
<div style="margin-top:1.25rem;padding:1rem;background:#f9fafb;border-radius:8px;font-size:.8rem;color:#6b7280">
<strong style="color:#111;display:block;margin-bottom:.35rem">Waiver Link</strong>
<code style="word-break:break-all;font-size:.72rem">https://parkerslingshotrentals.com/waiver.php?ref=<?= htmlspecialchars($b['booking_ref']) ?></code>
</div>
<?php endif; ?>
</div>
</div><!-- /.detail-panel -->
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</div>
<!-- Block Dates -->
<div class="card">
<div class="card-header"><h2>Block Dates</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, personal use">
</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><!-- /.main -->
<script>
// ── Expand/collapse detail panel ──────────────────────────────────────────────
function toggleDetail(id) {
const row = document.getElementById('detail-' + id);
const btn = document.getElementById('expand-' + id);
const open = row.style.display !== 'table-row';
row.style.display = open ? 'table-row' : 'none';
row.classList.toggle('open', open);
btn.classList.toggle('open', open);
}
// ── Status dropdown ───────────────────────────────────────────────────────────
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'); });
}
// ── Toggle requirement (insurance / deposit / license) ────────────────────────
function toggleReq(id, field, btn) {
btn.disabled = true;
fetch('/admin/', {
method:'POST',
headers:{'Content-Type':'application/x-www-form-urlencoded','X-Requested-With':'XMLHttpRequest'},
body:'action=toggle_requirement&id='+id+'&field='+field
})
.then(r=>r.json())
.then(d=>{
if (!d.ok) { alert('Error saving'); btn.disabled=false; return; }
const on = d.value === 1;
// Update button
btn.classList.toggle('active', on);
const labels = {
insurance_verified: ['Mark Received','✓ Marked Received'],
deposit_received: ['Mark Deposit Received','✓ Deposit Received'],
license_verified: ['Mark License Verified','✓ License Verified'],
};
btn.textContent = on ? labels[field][1] : labels[field][0];
btn.disabled = false;
// Update icon
const icon = document.getElementById('icon-'+id+'-'+field);
if (icon) {
icon.className = 'flow-icon ' + (on ? 'done' : 'pending');
icon.textContent = on ? '✓' : icon.textContent.replace('✓','').trim() || '?';
// Restore step numbers for known fields
const stepNums = {insurance_verified:'4', deposit_received:'5', license_verified:'6'};
if (!on) icon.textContent = stepNums[field] || '?';
}
// Update meta text
const meta = document.getElementById('meta-'+id+'-'+field);
const metaTexts = {
insurance_verified: ['Pending — verify at pickup','Verified — on file'],
deposit_received: ['Pending — collect at pickup','Deposit received'],
license_verified: ['Verify at pickup','License verified'],
};
if (meta && metaTexts[field]) meta.textContent = on ? metaTexts[field][1] : metaTexts[field][0];
// Update progress dot in table row
const dot = document.getElementById('dot-'+id+'-'+field);
if (dot) {
dot.className = 'dot ' + (on ? 'dot-done' : 'dot-pending');
}
// Auto-uncheck reminder checkbox if item is now done
const reminderBox = document.getElementById('reminder-'+id);
if (reminderBox && on) {
const fieldToItem = {insurance_verified:'insurance', deposit_received:'deposit', license_verified:'license'};
const cb = reminderBox.querySelector('input[value="'+fieldToItem[field]+'"]');
if (cb) cb.checked = false;
}
});
}
// ── Admin notes ───────────────────────────────────────────────────────────────
function saveNotes(id) {
const ta = document.getElementById('notes-' + id);
const btn = ta.nextElementSibling;
fetch('/admin/', {
method:'POST',
headers:{'Content-Type':'application/x-www-form-urlencoded','X-Requested-With':'XMLHttpRequest'},
body:'action=save_admin_notes&id='+id+'&notes='+encodeURIComponent(ta.value)
}).then(r=>r.json()).then(d=>{
btn.textContent = d.ok ? 'Saved ✓' : 'Error';
setTimeout(()=>btn.textContent='Save Notes', 1800);
});
}
// ── Send reminder email ───────────────────────────────────────────────────────
function sendReminder(id) {
const box = document.getElementById('reminder-' + id);
const btn = document.getElementById('remind-btn-' + id);
const status= document.getElementById('remind-status-' + id);
const checks= Array.from(box.querySelectorAll('input[type=checkbox]:checked')).map(c=>c.value);
if (!checks.length) { alert('Please select at least one item to include in the reminder.'); return; }
if (!confirm('Send reminder email about: ' + checks.join(', ') + '?')) return;
btn.disabled = true;
btn.textContent = 'Sending…';
fetch('/admin/', {
method:'POST',
headers:{'Content-Type':'application/x-www-form-urlencoded','X-Requested-With':'XMLHttpRequest'},
body:'action=send_reminder&id='+id+'&items='+checks.join(',')
}).then(r=>r.json()).then(d=>{
btn.disabled = false;
if (d.ok) {
btn.textContent = 'Reminder Sent ✓';
btn.style.background = '#16a34a';
status.style.display = 'block';
status.style.color = '#16a34a';
status.textContent = 'Email sent successfully.';
setTimeout(()=>{
btn.textContent = 'Send Reminder Email';
btn.style.background = '';
status.style.display = 'none';
}, 4000);
} else {
btn.textContent = 'Send Reminder Email';
alert(d.error || 'Failed to send reminder.');
}
});
}
// ── Square payment actions (capture / void / refund) ─────────────────────────
function squareAction(id, action, btn) {
const labels = {
square_capture: ['Capture', 'Capturing…', 'Captured ✓'],
square_void: ['Void Hold', 'Voiding…', 'Voided ✓'],
square_refund: ['Refund', 'Refunding…', 'Refunded ✓'],
};
const [orig, working, done] = labels[action];
const confirmMsg = {
square_capture: 'Charge the deposit hold to this card?',
square_void: 'Void the deposit hold? The customer will NOT be charged.',
square_refund: 'Refund the full deposit to this card?',
}[action];
if (!confirm(confirmMsg)) return;
btn.disabled = true;
btn.textContent = working;
fetch('/admin/', {
method:'POST',
headers:{'Content-Type':'application/x-www-form-urlencoded','X-Requested-With':'XMLHttpRequest'},
body:'action='+action+'&id='+id
}).then(r=>r.json()).then(d=>{
if (d.ok) {
btn.textContent = done;
btn.style.background = '#16a34a';
btn.style.color = '#fff';
btn.style.border = 'none';
// Update meta text and dot
const meta = document.getElementById('meta-'+id+'-deposit_received');
const icon = document.getElementById('icon-'+id+'-deposit_received');
const dot = document.getElementById('dot-'+id+'-deposit_received');
if (d.status === 'COMPLETED') {
if (meta) meta.textContent = 'Captured — deposit charged';
if (icon) { icon.className='flow-icon done'; icon.textContent='✓'; }
if (dot) dot.className='dot dot-done';
// Replace action area with refund button
const area = document.getElementById('deposit-actions-'+id);
if (area) area.innerHTML = '<button class="flow-toggle" onclick="squareAction('+id+',\'square_refund\',this)" style="border-color:#2563eb;color:#2563eb">Refund Deposit</button>';
} else if (d.status === 'CANCELED') {
if (meta) meta.textContent = 'Hold voided — no charge';
if (icon) { icon.className='flow-icon skip'; icon.textContent='—'; }
if (dot) dot.className='dot dot-skip';
const area = document.getElementById('deposit-actions-'+id);
if (area) area.innerHTML = '';
} else if (d.status === 'REFUNDED') {
if (meta) meta.textContent = 'Refunded — deposit returned';
if (icon) { icon.className='flow-icon pending'; icon.textContent='↩'; }
if (dot) dot.className='dot dot-skip';
const area = document.getElementById('deposit-actions-'+id);
if (area) area.innerHTML = '';
}
} else {
btn.textContent = orig;
btn.disabled = false;
alert('Error: ' + (d.error || 'Unknown error'));
}
}).catch(()=>{
btn.textContent = orig;
btn.disabled = false;
alert('Request failed. Please try again.');
});
}
// ── Block / unblock dates ─────────────────────────────────────────────────────
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>