Files
parkerslingshot/admin/index.php
T

2257 lines
126 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
// Force no-cache BEFORE any output (OLS .htaccess Header directives are unreliable)
header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
header('Pragma: no-cache');
header('Expires: Thu, 01 Jan 1970 00:00:00 GMT');
require_once dirname(__DIR__) . '/db.php';
// ── URL-token auth (no cookies — works in all browsers) ───────────────────────
function _createToken(): string {
$token = bin2hex(random_bytes(32));
db()->prepare("INSERT INTO admin_tokens (token, expires_at) VALUES (?, ?)")
->execute([$token, date('Y-m-d H:i:s', time() + 86400)]);
db()->exec("DELETE FROM admin_tokens WHERE expires_at < NOW()");
return $token;
}
function _verifyToken(string $token): bool {
if (!preg_match('/^[a-f0-9]{64}$/', $token)) return false;
$stmt = db()->prepare("SELECT token FROM admin_tokens WHERE token=? AND expires_at > NOW()");
$stmt->execute([$token]);
return (bool)$stmt->fetch();
}
$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)) {
$t = _createToken();
header('Location: /admin/?_t=' . $t);
} else {
header('Location: /admin/?err=1');
}
exit;
}
$rawToken = preg_replace('/[^a-f0-9]/', '', $_GET['_t'] ?? $_POST['_t'] ?? '');
if (($_GET['action'] ?? '') === 'logout') {
if ($rawToken) db()->prepare("DELETE FROM admin_tokens WHERE token=?")->execute([$rawToken]);
header('Location: /admin/'); exit;
}
$authed = $rawToken !== '' && _verifyToken($rawToken);
$token = $authed ? $rawToken : '';
// ── AJAX handlers ─────────────────────────────────────────────────────────────
if ($isAjax && !$authed) {
http_response_code(401);
header('Content-Type: application/json');
echo json_encode(['error'=>'Session expired. Please 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','helmet_provided','safety_course','operational_course'];
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://parkerslingshot.epictravelexpeditions.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 show proof of valid personal auto insurance at pickup. To speed things up, you can upload it now — a photo of your insurance card is fine.',
'cta' => "<div style='margin-top:10px'><a href='" . SITE_URL . "/upload-docs.php?ref={$ref}&type=insurance' style='display:inline-block;background:#f97316;color:#fff;text-decoration:none;padding:9px 20px;border-radius:6px;font-weight:700;font-size:13px'>Upload Insurance Card &rarr;</a></div>",
],
'deposit' => [
'label' => 'Balance Due at Pickup',
'detail' => 'Your $' . number_format(DEPOSIT_AMOUNT, 2) . ' deposit hold has been placed on your card. The remaining balance is due at pickup — cash or card accepted. Your deposit hold will be released upon safe return of the vehicle.',
'cta' => '',
],
'license' => [
'label' => "Valid Driver's License",
'detail' => "Please bring your valid driver's license to pickup — we'll verify it before you head out. You can also upload a photo in advance to keep things moving at arrival.",
'cta' => "<div style='margin-top:10px'><a href='" . SITE_URL . "/upload-docs.php?ref={$ref}&type=license' style='display:inline-block;background:#f97316;color:#fff;text-decoration:none;padding:9px 20px;border-radius:6px;font-weight:700;font-size:13px'>Upload License Photo &rarr;</a></div>",
],
];
$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) 266-2022</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);
if ($sent) {
echo json_encode(['ok'=>true]);
} else {
echo json_encode(['ok'=>false,'error'=>'Email service not configured — check CYBERMAIL_API_KEY in db.php.']);
}
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', status='cancelled' 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, status='cancelled' 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 === 'charge_balance') {
$id = (int)($_POST['id'] ?? 0);
$stmt = db()->prepare("SELECT * FROM bookings WHERE id=?");
$stmt->execute([$id]);
$b = $stmt->fetch();
if (!$b) { echo json_encode(['error'=>'Not found']); exit; }
$cardId = $b['square_card_id'] ?? '';
$customerId = $b['square_customer_id'] ?? '';
if (!$cardId) { echo json_encode(['error'=>'No card on file']); exit; }
$balance = (float)$b['amount'] - DEPOSIT_AMOUNT;
if ($balance <= 0) { echo json_encode(['error'=>'No balance due']); exit; }
$cents = (int)($balance * 100);
$payBody = [
'source_id' => $cardId,
'idempotency_key' => $b['booking_ref'] . '-bal-' . time(),
'amount_money' => ['amount' => $cents, 'currency' => 'USD'],
'autocomplete' => true,
'location_id' => SQUARE_LOCATION_ID,
'note' => "Balance charge — booking " . $b['booking_ref'],
'reference_id' => $b['booking_ref'],
];
if ($customerId) $payBody['customer_id'] = $customerId;
$resp = squareApi('POST', '/payments', $payBody);
if (!empty($resp['payment']['id'])) {
$pid = $resp['payment']['id'];
db()->prepare("UPDATE bookings SET square_payment_status='BAL_COMPLETED', deposit_received=1 WHERE id=?")
->execute([$id]);
echo json_encode(['ok'=>true,'payment_id'=>$pid,'amount'=>$balance]);
} else {
$errCode = $resp['errors'][0]['code'] ?? '';
$errMsg = match($errCode) {
'CARD_DECLINED','CARD_DECLINED_VERIFICATION_REQUIRED' => 'Card was declined.',
'INSUFFICIENT_FUNDS' => 'Insufficient funds.',
default => $resp['errors'][0]['detail'] ?? 'Charge failed.',
};
echo json_encode(['error'=>$errMsg]);
}
exit;
}
if ($action === 'update_card') {
$id = (int)($_POST['id'] ?? 0);
$nonce = trim($_POST['nonce'] ?? '');
if (!$id || !$nonce) { echo json_encode(['error'=>'Missing data']); exit; }
$stmt = db()->prepare("SELECT * FROM bookings WHERE id=?");
$stmt->execute([$id]);
$b = $stmt->fetch();
if (!$b) { echo json_encode(['error'=>'Not found']); exit; }
// Disable old card if exists
$oldCardId = $b['square_card_id'] ?? '';
if ($oldCardId) squareApi('POST', "/cards/{$oldCardId}/disable");
// Get or create Square customer
$customerId = $b['square_customer_id'] ?? '';
if (!$customerId) {
$custResp = squareApi('POST', '/customers', [
'idempotency_key' => $b['booking_ref'] . '-cust2',
'given_name' => $b['name'],
'email_address' => $b['email'],
'phone_number' => $b['phone'] ?: null,
'reference_id' => $b['booking_ref'],
]);
$customerId = $custResp['customer']['id'] ?? null;
}
// Create new card on file
$cardBody = ['idempotency_key'=>$b['booking_ref'].'-card-'.time(),'source_id'=>$nonce,'card'=>['cardholder_name'=>$b['name']]];
if ($customerId) $cardBody['card']['customer_id'] = $customerId;
$cardResp = squareApi('POST', '/cards', $cardBody);
$newCardId = $cardResp['card']['id'] ?? null;
$last4 = $cardResp['card']['last_4'] ?? null;
$brand = $cardResp['card']['card_brand'] ?? null;
if (!$newCardId) {
echo json_encode(['error' => $cardResp['errors'][0]['detail'] ?? 'Could not save card']); exit;
}
db()->prepare("UPDATE bookings SET square_customer_id=?, square_card_id=?, card_last4=?, card_brand=? WHERE id=?")
->execute([$customerId, $newCardId, $last4, $brand, $id]);
echo json_encode(['ok'=>true,'last4'=>$last4,'brand'=>$brand]);
exit;
}
if ($action === 'mark_returned') {
$id = (int)($_POST['id'] ?? 0);
if (!$id) { echo json_encode(['error'=>'Invalid']); exit; }
$stmt = db()->prepare("SELECT * FROM bookings WHERE id=?");
$stmt->execute([$id]);
$b = $stmt->fetch();
if (!$b) { echo json_encode(['error'=>'Not found']); exit; }
// Mark returned, set completed, wipe card data
db()->prepare("UPDATE bookings SET slingshot_returned=1, returned_at=NOW(), status='completed',
square_card_id=NULL, card_last4=NULL, card_brand=NULL, square_customer_id=NULL
WHERE id=?")
->execute([$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]);
$newId = (int)db()->lastInsertId();
echo json_encode(['ok'=>true, 'id'=>$newId, 'date'=>$date, 'reason'=>$reason]);
} 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;
}
if ($action === 'update_email') {
$id = (int)($_POST['id'] ?? 0);
$email = trim($_POST['email'] ?? '');
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { echo json_encode(['error'=>'Invalid email']); exit; }
db()->prepare("UPDATE bookings SET email=? WHERE id=?")->execute([$email, $id]);
echo json_encode(['ok'=>true,'email'=>$email]);
exit;
}
if ($action === 'resend_confirmation') {
$id = (int)($_POST['id'] ?? 0);
$email = trim($_POST['email'] ?? '');
$stmt = db()->prepare("SELECT * FROM bookings WHERE id=?");
$stmt->execute([$id]);
$b = $stmt->fetch();
if (!$b) { echo json_encode(['error'=>'Not found']); exit; }
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) $email = $b['email'];
$pkg = PACKAGES[$b['package']] ?? ['label'=>$b['package'],'price'=>$b['amount']];
$dateLabel = date('F j, Y', strtotime($b['rental_date']));
$ref = $b['booking_ref'];
$amtLabel = '$' . number_format((float)$b['amount'], 2);
$depLabel = '$' . number_format(DEPOSIT_AMOUNT, 2);
$balLabel = '$' . number_format(max(0, (float)$b['amount'] - DEPOSIT_AMOUNT), 2);
$confirmHtml = "<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:#0d0d0d'>Booking Confirmation</h2>
<p style='color:#374151'>Hey " . htmlspecialchars($b['name']) . ", here's a copy of your booking confirmation.</p>
<div style='background:#fff7ed;border:1px solid #fed7aa;border-radius:10px;padding:20px;margin:20px 0'>
<p style='margin:0 0 6px;font-size:13px;color:#9ca3af;text-transform:uppercase;letter-spacing:1px'>Booking Reference</p>
<p style='margin:0 0 16px;font-size:22px;font-weight:700;color:#f97316'>{$ref}</p>
<p style='margin:4px 0;font-size:14px;color:#374151'><strong>Package:</strong> " . htmlspecialchars($pkg['label']) . "</p>
<p style='margin:4px 0;font-size:14px;color:#374151'><strong>Date:</strong> {$dateLabel}</p>
<p style='margin:4px 0;font-size:14px;color:#374151'><strong>Total:</strong> {$amtLabel}</p>
<p style='margin:4px 0;font-size:14px;color:#374151'><strong>Deposit hold:</strong> {$depLabel}</p>
<p style='margin:4px 0;font-size:14px;color:#374151'><strong>Balance at pickup:</strong> <span style='font-weight:700;color:#16a34a'>{$balLabel}</span></p>
</div>
<div style='margin:20px 0;padding:16px;background:#fff7ed;border:1px solid #fed7aa;border-radius:10px;text-align:center'>
<p style='margin:0 0 10px;font-size:14px;font-weight:700;color:#111'>Sign Your Rental Agreement</p>
<a href='" . SITE_URL . "/waiver.php?ref={$ref}' style='display:inline-block;background:#f97316;color:#fff;text-decoration:none;padding:10px 24px;border-radius:6px;font-weight:700;font-size:14px'>Sign Rental Agreement &rarr;</a>
</div>
<p style='color:#374151'>Questions? Call or text <strong>" . ADMIN_PHONE . "</strong> or reply to this email.</p>
<p style='color:#374151'>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($email, $b['name'], "Booking Confirmation {$ref} — Parker County Slingshot Rentals", $confirmHtml);
echo json_encode($sent ? ['ok'=>true,'email'=>$email] : ['error'=>'Email send failed — check Mailjet credentials']);
exit;
}
if ($action === 'customer_save') {
$cid = (int)($_POST['id'] ?? 0);
$name = substr(trim($_POST['name'] ?? ''), 0, 150);
$email = trim($_POST['email'] ?? '');
$phone = substr(trim($_POST['phone'] ?? ''), 0, 30);
$dob = preg_match('/^\d{4}-\d{2}-\d{2}$/', $_POST['dob'] ?? '') ? $_POST['dob'] : null;
$addr = substr(trim($_POST['address'] ?? ''), 0, 500);
$notes = substr(trim($_POST['notes'] ?? ''), 0, 1000);
$active = (int)($_POST['is_active'] ?? 1);
if (!$name || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
echo json_encode(['error' => 'Name and valid email are required']); exit;
}
if ($cid) {
db()->prepare("UPDATE pcs_customers SET name=?,email=?,phone=?,dob=?,address=?,notes=?,is_active=? WHERE id=?")
->execute([$name,$email,$phone,$dob,$addr,$notes,$active,$cid]);
} else {
db()->prepare("INSERT INTO pcs_customers (name,email,phone,dob,address,notes,is_active) VALUES (?,?,?,?,?,?,?)")
->execute([$name,$email,$phone,$dob,$addr,$notes,1]);
$cid = (int)db()->lastInsertId();
}
echo json_encode(['ok'=>true,'id'=>$cid]); exit;
}
if ($action === 'customer_delete') {
$cid = (int)($_POST['id'] ?? 0);
if ($cid) db()->prepare("DELETE FROM pcs_customers WHERE id=?")->execute([$cid]);
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>
<?php if (!empty($_GET['err'])): ?>
<p style="color:#f87171;margin-bottom:1rem">Invalid username or password.</p>
<?php endif; ?>
<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 ─────────────────────────────────────────────────────────────
$bookings = db()->query("SELECT * FROM bookings ORDER BY rental_date ASC, created_at DESC")->fetchAll();
$blocked = db()->query("SELECT * FROM blocked_dates ORDER BY block_date ASC")->fetchAll();
$customers = db()->query("
SELECT c.*
FROM pcs_customers c ORDER BY c.created_at DESC
")->fetchAll();
// Group bookings by customer email in PHP (avoids cross-table collation issues)
$bookingsByEmail = [];
foreach ($bookings as $_b) {
$bookingsByEmail[strtolower(trim($_b['email']))][] = $_b;
}
$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 .filter-btn{padding:.35rem .75rem;border-radius:6px;font-size:.8rem;color:#6b7280;border:1px solid #e5e7eb;margin-left:.35rem;background:#fff;cursor:pointer;font-family:inherit}
.filters .filter-btn.active,.filters .filter-btn: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}}
/* Customer card */
.cust-search{border:1px solid #e5e7eb;border-radius:6px;padding:.45rem .75rem;font-size:.875rem;width:100%;max-width:280px}
.cust-form{background:#f9fafb;border-top:1px solid #f3f4f6;padding:1.25rem 1.5rem;display:none}
.cust-form.open{display:block}
.cust-form .fg{display:grid;grid-template-columns:1fr 1fr;gap:.75rem;margin-bottom:.75rem}
@media(max-width:600px){.cust-form .fg{grid-template-columns:1fr}}
.cust-form label{font-size:.78rem;color:#6b7280;display:block;margin-bottom:.2rem}
.cust-form input,.cust-form textarea{width:100%;border:1px solid #e5e7eb;border-radius:6px;padding:.45rem .65rem;font-size:.875rem;font-family:inherit;outline:none}
.cust-form input:focus,.cust-form textarea:focus{border-color:#f97316}
.cust-form textarea{resize:vertical;min-height:60px}
.cust-form .form-actions{display:flex;gap:.5rem;margin-top:.75rem}
.cust-badge{display:inline-block;padding:.15rem .5rem;border-radius:999px;font-size:.7rem;font-weight:700}
.cust-active{background:#dcfce7;color:#15803d}
.cust-inactive{background:#f3f4f6;color:#9ca3af}
/* Customer expandable detail */
.cust-detail-panel{display:grid;grid-template-columns:250px 1fr;border-top:2px solid #f97316}
@media(max-width:900px){.cust-detail-panel{grid-template-columns:1fr}}
.cdp-info{padding:1.5rem;border-right:1px solid #f3f4f6;background:#f9fafb}
.cdp-info h3,.cdp-bookings h3{font-size:.72rem;font-weight:700;text-transform:uppercase;letter-spacing:1px;color:#9ca3af;margin-bottom:1rem}
.cdp-bookings{padding:1.5rem;min-width:0}
.cust-booking-card{border:1px solid #e5e7eb;border-radius:8px;margin-bottom:1.25rem;overflow:hidden}
.cust-booking-card:last-child{margin-bottom:0}
.cbc-header{display:flex;align-items:center;gap:.65rem;padding:.7rem 1rem;background:#f9fafb;border-bottom:1px solid #f3f4f6;flex-wrap:wrap}
.cbc-flow{padding:.5rem 1rem;border-bottom:1px solid #f3f4f6}
.cbc-notes{padding:.75rem 1rem}
</style>
</head>
<body>
<header>
<h1>Parker County Slingshot — Admin</h1>
<a href="?action=logout&_t=<?= htmlspecialchars($token) ?>">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">
<button class="filter-btn active" onclick="filterBookings('all',this)">All</button>
<button class="filter-btn" onclick="filterBookings('pending',this)">Pending</button>
<button class="filter-btn" onclick="filterBookings('confirmed',this)">Confirmed</button>
<button class="filter-btn" onclick="filterBookings('completed',this)">Completed</button>
<button class="filter-btn" onclick="filterBookings('cancelled',this)">Cancelled</button>
</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'];
$insFile = $b['insurance_file'] ?? '';
$stepDeposit = (bool)$b['deposit_received'];
$stepLicense = (bool)$b['license_verified'];
$licFile = $b['license_file'] ?? '';
$stepHelmet = (bool)$b['helmet_provided'];
$stepSafety = (bool)$b['safety_course'];
$stepOps = (bool)$b['operational_course'];
$stepReturned = (bool)$b['slingshot_returned'];
// 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 && $stepHelmet && $stepSafety && $stepOps && $stepReturned;
$pendingCount = ($cancelled ? 0 : (
(!$stepConfirmed?1:0)+(!$stepWaiver?1:0)+(!$stepInsurance?1:0)+(!$stepDeposit?1:0)+(!$stepLicense?1:0)+
(!$stepHelmet?1:0)+(!$stepSafety?1:0)+(!$stepOps?1:0)+(!$stepReturned?1:0)
));
?>
<tr class="booking-row" data-status="<?= htmlspecialchars($b['status']) ?>" 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 / License / Deposit / Helmet / Safety / Ops / Returned">
<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($stepLicense) ?>" title="License" id="dot-<?= $bid ?>-license_verified"></div>
<div class="dot <?= $dotClass($stepDeposit) ?>" title="Deposit" id="dot-<?= $bid ?>-deposit_received"></div>
<div class="dot <?= $dotClass($stepHelmet) ?>" title="Helmet" id="dot-<?= $bid ?>-helmet_provided"></div>
<div class="dot <?= $dotClass($stepSafety) ?>" title="Safety Course" id="dot-<?= $bid ?>-safety_course"></div>
<div class="dot <?= $dotClass($stepOps) ?>" title="Ops Course" id="dot-<?= $bid ?>-operational_course"></div>
<div class="dot <?= $cancelled||!$stepReturned?($cancelled?'dot-skip':'dot-pending'):'dot-done' ?>" title="Returned" id="dot-<?= $bid ?>-slingshot_returned"></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 ?>" data-status="<?= htmlspecialchars($b['status']) ?>">
<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') ?>" id="icon-<?= $bid ?>-confirmed">
<?= $stepConfirmed?'✓':($cancelled?'—':'2') ?>
</div>
<div class="flow-body">
<span class="flow-label">Booking Confirmed</span>
<span class="flow-meta" id="meta-<?= $bid ?>-confirmed">
<?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://parkerslingshot.epictravelexpeditions.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':($insFile?'pending':'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':($insFile?'Doc submitted — verify at pickup':'Pending — verify at pickup')) ?>
</span>
<?php if (!$cancelled): ?>
<div class="flow-action" style="margin-top:4px;display:flex;flex-wrap:wrap;gap:6px;align-items:center">
<?php if ($insFile): ?>
<a class="flow-toggle" href="/view-doc.php?ref=<?= urlencode($b['booking_ref']) ?>&type=insurance&_t=<?= $token ?>" target="_blank" style="text-decoration:none">📄 View Submitted Doc ↗</a>
<?php endif; ?>
<button class="flow-toggle <?= $stepInsurance?'active':'' ?>"
id="btn-<?= $bid ?>-insurance_verified"
onclick="toggleReq(<?= $bid ?>,'insurance_verified',this)">
<?= $stepInsurance?'✓ Verified at Pickup':'Mark Verified at Pickup' ?>
</button>
<button class="flow-toggle" onclick="openUploadLink('<?= htmlspecialchars($b['booking_ref']) ?>','insurance')">📎 Upload Link</button>
</div>
<?php endif; ?>
</div>
</div>
<!-- Step 5: License -->
<div class="flow-step">
<div class="flow-icon <?= $stepLicense?'done':($cancelled?'skip':'pending') ?>" id="icon-<?= $bid ?>-license_verified">
<?= $stepLicense?'✓':($cancelled?'—':'5') ?>
</div>
<div class="flow-body">
<span class="flow-label">Driver's License Verified</span>
<span class="flow-meta" id="meta-<?= $bid ?>-license_verified">
<?= $stepLicense?'Verified at pickup':($cancelled?'N/A':($licFile?'Doc submitted — verify at pickup':'Verify at pickup')) ?>
</span>
<?php if (!$cancelled): ?>
<div class="flow-action" style="margin-top:4px;display:flex;flex-wrap:wrap;gap:6px;align-items:center">
<?php if ($licFile): ?>
<a class="flow-toggle" href="/view-doc.php?ref=<?= urlencode($b['booking_ref']) ?>&type=license&_t=<?= $token ?>" target="_blank" style="text-decoration:none">📄 View Submitted Doc ↗</a>
<?php endif; ?>
<button class="flow-toggle <?= $stepLicense?'active':'' ?>"
id="btn-<?= $bid ?>-license_verified"
onclick="toggleReq(<?= $bid ?>,'license_verified',this)">
<?= $stepLicense?'✓ Verified at Pickup':'Mark Verified at Pickup' ?>
</button>
<button class="flow-toggle" onclick="openUploadLink('<?= htmlspecialchars($b['booking_ref']) ?>','license')">📎 Upload Link</button>
</div>
<?php endif; ?>
</div>
</div>
<!-- Step 6: Deposit (Square-aware) -->
<?php
$sqStatus = $b['square_payment_status'] ?? '';
$sqId = $b['square_payment_id'] ?? '';
$sqCardId = $b['square_card_id'] ?? '';
$cardLast4 = $b['card_last4'] ?? '';
$cardBrand = $b['card_brand'] ?? '';
$depositIcon = 'pending';
$depositLabel = '6';
if ($cancelled) { $depositIcon='skip'; $depositLabel='—'; }
elseif ($stepDeposit ||
in_array($sqStatus,['COMPLETED','BAL_COMPLETED'])) { $depositIcon='done'; $depositLabel='✓'; }
$priorStepsDone = $stepConfirmed && $stepWaiver && $stepInsurance && $stepLicense;
?>
<div class="flow-step">
<div class="flow-icon <?= $depositIcon ?>" id="icon-<?= $bid ?>-deposit_received">
<?= $depositLabel ?>
</div>
<div class="flow-body">
<span class="flow-label">Deposit &amp; Balance — $<?= number_format(DEPOSIT_AMOUNT,2) ?> held · $<?= number_format($b['amount']-DEPOSIT_AMOUNT,2) ?> at pickup</span>
<?php if ($cardLast4): ?>
<span style="font-size:.72rem;color:#16a34a;font-weight:600;display:block;margin-top:2px">
<?= htmlspecialchars($cardBrand) ?> •••• <?= htmlspecialchars($cardLast4) ?> on file
<button class="flow-link" style="font-size:.7rem;margin-left:6px;background:none;border:none;cursor:pointer;color:#f97316;text-decoration:underline;padding:0" onclick="openUpdateCard(<?= $bid ?>, this)">Update Card</button>
</span>
<?php endif; ?>
<span class="flow-meta" id="meta-<?= $bid ?>-deposit_received">
<?php if ($cancelled): ?>N/A
<?php elseif ($sqStatus === 'BAL_COMPLETED'): ?>Balance charged to card on file
<?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 ($sqStatus === 'DECLINED'): ?>Card was declined — contact customer
<?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 &amp; Cancel Booking</button>
<?php if ($sqCardId && $priorStepsDone): ?>
<button class="flow-toggle" onclick="chargeBalance(<?= $bid ?>,this)" style="margin-left:4px;border-color:#16a34a;color:#16a34a">Charge Balance $<?= number_format($b['amount']-DEPOSIT_AMOUNT,2) ?></button>
<?php elseif ($sqCardId): ?>
<button class="flow-toggle" disabled style="margin-left:4px;border-color:#9ca3af;color:#9ca3af;opacity:.55;cursor:not-allowed" title="Complete steps 15 before charging balance">Charge Balance $<?= number_format($b['amount']-DEPOSIT_AMOUNT,2) ?></button>
<?php endif; ?>
<?php elseif ($sqStatus === 'COMPLETED'): ?>
<button class="flow-toggle" onclick="squareAction(<?= $bid ?>,'square_refund',this)" style="border-color:#2563eb;color:#2563eb">Refund &amp; Cancel Booking</button>
<?php if ($sqCardId && $priorStepsDone): ?>
<button class="flow-toggle" onclick="chargeBalance(<?= $bid ?>,this)" style="margin-left:4px;border-color:#16a34a;color:#16a34a">Charge Balance $<?= number_format($b['amount']-DEPOSIT_AMOUNT,2) ?></button>
<?php elseif ($sqCardId): ?>
<button class="flow-toggle" disabled style="margin-left:4px;border-color:#9ca3af;color:#9ca3af;opacity:.55;cursor:not-allowed" title="Complete steps 15 before charging balance">Charge Balance $<?= number_format($b['amount']-DEPOSIT_AMOUNT,2) ?></button>
<?php endif; ?>
<?php elseif (!$sqId || $sqStatus === 'DECLINED'): ?>
<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>
<?php if (!$cancelled): ?>
<!-- ── Pre-departure checklist (admin only, after deposit confirmed) ── -->
<div style="margin:.6rem 0 .2rem;padding:.4rem .5rem;background:#fff7ed;border-radius:6px;font-size:.7rem;font-weight:700;text-transform:uppercase;letter-spacing:.5px;color:#92400e">
Pre-Departure Checklist
</div>
<!-- Step 7: DOT Helmet -->
<div class="flow-step">
<div class="flow-icon <?= $stepHelmet?'done':'pending' ?>" id="icon-<?= $bid ?>-helmet_provided">
<?= $stepHelmet?'✓':'7' ?>
</div>
<div class="flow-body">
<span class="flow-label">DOT Helmet Provided &amp; Fits</span>
<span class="flow-meta" id="meta-<?= $bid ?>-helmet_provided">
<?= $stepHelmet?'Helmet provided and fitted':'Provide DOT helmet — verify fit before departure' ?>
</span>
<div class="flow-action">
<button class="flow-toggle <?= $stepHelmet?'active':'' ?>"
id="btn-<?= $bid ?>-helmet_provided"
onclick="toggleReq(<?= $bid ?>,'helmet_provided',this)">
<?= $stepHelmet?'✓ Helmet Provided':'Mark Helmet Provided' ?>
</button>
</div>
</div>
</div>
<!-- Step 8: Safety Course -->
<div class="flow-step">
<div class="flow-icon <?= $stepSafety?'done':'pending' ?>" id="icon-<?= $bid ?>-safety_course">
<?= $stepSafety?'✓':'8' ?>
</div>
<div class="flow-body">
<span class="flow-label">Safety Course Completed</span>
<span class="flow-meta" id="meta-<?= $bid ?>-safety_course">
<?= $stepSafety?'Safety course completed':'Complete safety briefing before departure' ?>
</span>
<div class="flow-action">
<button class="flow-toggle <?= $stepSafety?'active':'' ?>"
id="btn-<?= $bid ?>-safety_course"
onclick="toggleReq(<?= $bid ?>,'safety_course',this)">
<?= $stepSafety?'✓ Safety Course Done':'Mark Safety Course Done' ?>
</button>
</div>
</div>
</div>
<!-- Step 9: Operational Course -->
<div class="flow-step">
<div class="flow-icon <?= $stepOps?'done':'pending' ?>" id="icon-<?= $bid ?>-operational_course">
<?= $stepOps?'✓':'9' ?>
</div>
<div class="flow-body">
<span class="flow-label">Slingshot Operational Course Done</span>
<span class="flow-meta" id="meta-<?= $bid ?>-operational_course">
<?= $stepOps?'Operational course completed':'Walk customer through Slingshot controls before departure' ?>
</span>
<div class="flow-action">
<button class="flow-toggle <?= $stepOps?'active':'' ?>"
id="btn-<?= $bid ?>-operational_course"
onclick="toggleReq(<?= $bid ?>,'operational_course',this)">
<?= $stepOps?'✓ Ops Course Done':'Mark Ops Course Done' ?>
</button>
</div>
</div>
</div>
<!-- Step 10: Slingshot Returned -->
<div class="flow-step">
<div class="flow-icon <?= $stepReturned?'done':'pending' ?>" id="icon-<?= $bid ?>-slingshot_returned">
<?= $stepReturned?'✓':'10' ?>
</div>
<div class="flow-body">
<span class="flow-label">Slingshot Returned — Close Out</span>
<span class="flow-meta" id="meta-<?= $bid ?>-slingshot_returned">
<?php if ($stepReturned): ?>
Returned <?= $b['returned_at'] ? date('M j, Y g:ia', strtotime($b['returned_at'])) : '' ?> — booking closed, card data wiped
<?php else: ?>
Mark when slingshot is safely returned — closes booking &amp; wipes card data
<?php endif; ?>
</span>
<?php if (!$stepReturned): ?>
<div class="flow-action">
<button class="flow-toggle" style="border-color:#16a34a;color:#16a34a;font-weight:700"
onclick="markReturned(<?= $bid ?>,this)">
✓ Mark Slingshot Returned
</button>
</div>
<?php endif; ?>
</div>
</div>
<?php endif; ?>
</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="license" <?= !$stepLicense?'checked':'' ?>>
Bring driver's license
</label>
<label>
<input type="checkbox" value="deposit" <?= !$stepDeposit?'checked':'' ?>>
Security deposit info
</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://parkerslingshot.epictravelexpeditions.com/waiver.php?ref=<?= htmlspecialchars($b['booking_ref']) ?></code>
</div>
<div style="margin-top:1.25rem;padding:1rem;background:#f9fafb;border-radius:8px">
<strong style="color:#111;font-size:.8rem;display:block;margin-bottom:.5rem">Resend Confirmation Email</strong>
<div style="display:flex;gap:.5rem;align-items:center;flex-wrap:wrap">
<input type="email" id="resend-email-<?= $bid ?>" value="<?= htmlspecialchars($b['email']) ?>"
style="flex:1;min-width:180px;font-size:.82rem;padding:.45rem .65rem;border:1px solid #d1d5db;border-radius:6px;font-family:inherit" />
<button class="flow-toggle" onclick="resendConfirmation(<?= $bid ?>,this)" style="white-space:nowrap">Resend</button>
</div>
<div id="resend-status-<?= $bid ?>" style="margin-top:.4rem;font-size:.75rem;display:none"></div>
</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="blockDateInput" 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="blockReasonInput" 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>
<!-- Customers -->
<div class="card">
<div class="card-header">
<h2>Customers</h2>
<div style="display:flex;gap:.75rem;align-items:center;flex-wrap:wrap">
<input class="cust-search" type="search" id="custSearch" placeholder="Search name, email, phone…" oninput="filterCustomers(this.value)">
<button class="save-btn" onclick="toggleCustForm(0)" id="addCustBtn">+ Add Customer</button>
</div>
</div>
<!-- Add/Edit form -->
<div class="cust-form" id="custForm">
<input type="hidden" id="custId" value="0">
<div class="fg">
<div>
<label>Full Name *</label>
<input type="text" id="custName" placeholder="Jane Doe" maxlength="150">
</div>
<div>
<label>Email *</label>
<input type="email" id="custEmail" placeholder="jane@example.com">
</div>
<div>
<label>Phone</label>
<input type="tel" id="custPhone" placeholder="(817) 555-0100" maxlength="30">
</div>
<div>
<label>Date of Birth</label>
<input type="date" id="custDob">
</div>
<div style="grid-column:1/-1">
<label>Address</label>
<input type="text" id="custAddr" placeholder="123 Main St, Weatherford TX 76086" maxlength="500">
</div>
<div style="grid-column:1/-1">
<label>Notes</label>
<textarea id="custNotes" maxlength="1000" placeholder="e.g. Preferred customer, allergies, anything relevant…"></textarea>
</div>
</div>
<div class="form-actions">
<button class="save-btn" onclick="saveCustomer()">Save Customer</button>
<button class="save-btn" onclick="toggleCustForm(0)" style="background:#6b7280">Cancel</button>
</div>
<div id="custFormMsg" style="margin-top:.5rem;font-size:.8rem;display:none"></div>
</div>
<?php if (empty($customers)): ?>
<div style="padding:3rem;text-align:center;color:#9ca3af">No customers yet.</div>
<?php else: ?>
<div style="overflow-x:auto">
<table id="custTable">
<thead>
<tr>
<th style="width:36px"></th>
<th>Name</th>
<th>Email</th>
<th>Phone</th>
<th>Bookings</th>
<th>Status</th>
<th>Added</th>
<th style="width:60px"></th>
</tr>
</thead>
<tbody>
<?php foreach ($customers as $c):
$cid = $c['id'];
$custBookings = $bookingsByEmail[strtolower(trim($c['email']))] ?? [];
?>
<tr class="cust-row booking-row" data-cid="<?= $cid ?>"
data-search="<?= strtolower(htmlspecialchars($c['name'].' '.$c['email'].' '.($c['phone']??''))) ?>"
onclick="toggleCustDetail(<?= $cid ?>)">
<td onclick="event.stopPropagation()">
<button class="expand-btn" id="cexpand-<?= $cid ?>" onclick="toggleCustDetail(<?= $cid ?>)">&#9658;</button>
</td>
<td><strong><?= htmlspecialchars($c['name']) ?></strong></td>
<td style="font-size:.82rem"><a href="mailto:<?= htmlspecialchars($c['email']) ?>" onclick="event.stopPropagation()" style="color:#f97316;text-decoration:none"><?= htmlspecialchars($c['email']) ?></a></td>
<td style="font-size:.82rem"><?= htmlspecialchars($c['phone'] ?? '—') ?></td>
<td style="text-align:center"><?= count($custBookings) ?></td>
<td><span class="cust-badge <?= $c['is_active']?'cust-active':'cust-inactive' ?>"><?= $c['is_active']?'Active':'Inactive' ?></span></td>
<td style="font-size:.78rem;color:#9ca3af;white-space:nowrap"><?= date('M j, Y', strtotime($c['created_at'])) ?></td>
<td onclick="event.stopPropagation()" style="white-space:nowrap">
<button class="flow-toggle" style="font-size:.72rem;margin-right:3px"
onclick="editCustomer(<?= htmlspecialchars(json_encode($c), ENT_QUOTES) ?>)">Edit</button>
<button class="flow-toggle" style="font-size:.72rem;border-color:#dc2626;color:#dc2626"
onclick="deleteCustomer(<?= $cid ?>,this)">Del</button>
</td>
</tr>
<!-- ── Customer Detail Row ──────────────────────────────────────────── -->
<tr id="cdetail-<?= $cid ?>" style="display:none">
<td colspan="8" style="padding:0">
<div class="cust-detail-panel">
<!-- Left: Customer profile -->
<div class="cdp-info">
<h3>Customer Profile</h3>
<div class="ci-name"><?= htmlspecialchars($c['name']) ?></div>
<div class="ci-row"><a href="mailto:<?= htmlspecialchars($c['email']) ?>"><?= htmlspecialchars($c['email']) ?></a></div>
<?php if ($c['phone']): ?>
<div class="ci-row"><a href="tel:<?= htmlspecialchars($c['phone']) ?>"><?= htmlspecialchars($c['phone']) ?></a></div>
<?php endif; ?>
<?php if ($c['dob']): ?>
<div class="ci-field">Date of Birth</div>
<div style="font-size:.85rem"><?= date('F j, Y', strtotime($c['dob'])) ?></div>
<?php endif; ?>
<?php if ($c['address']): ?>
<div class="ci-field">Address</div>
<div style="font-size:.82rem;color:#6b7280"><?= nl2br(htmlspecialchars($c['address'])) ?></div>
<?php endif; ?>
<?php if ($c['notes']): ?>
<div class="ci-field">Notes</div>
<div style="font-size:.82rem;color:#6b7280;font-style:italic"><?= nl2br(htmlspecialchars($c['notes'])) ?></div>
<?php endif; ?>
<div style="margin-top:1.25rem;display:flex;gap:.5rem;align-items:center;flex-wrap:wrap">
<span class="cust-badge <?= $c['is_active']?'cust-active':'cust-inactive' ?>"><?= $c['is_active']?'Active':'Inactive' ?></span>
<button class="save-btn" style="font-size:.78rem"
onclick="editCustomer(<?= htmlspecialchars(json_encode($c), ENT_QUOTES) ?>)">Edit Profile</button>
</div>
</div>
<!-- Right: Booking history -->
<div class="cdp-bookings">
<h3>Booking History (<?= count($custBookings) ?>)</h3>
<?php if (empty($custBookings)): ?>
<p style="color:#9ca3af;font-size:.85rem">No bookings yet.</p>
<?php else: ?>
<?php foreach ($custBookings as $cb):
$bid = $cb['id'];
$pkg = PACKAGES[$cb['package']] ?? ['label'=>$cb['package']];
$stepConfirmed = in_array($cb['status'], ['confirmed','completed']);
$stepWaiver = (bool)$cb['waiver_signed'];
$stepInsurance = (bool)$cb['insurance_verified'];
$insFile = $cb['insurance_file'] ?? '';
$stepDeposit = (bool)$cb['deposit_received'];
$stepLicense = (bool)$cb['license_verified'];
$licFile = $cb['license_file'] ?? '';
$stepHelmet = (bool)$cb['helmet_provided'];
$stepSafety = (bool)$cb['safety_course'];
$stepOps = (bool)$cb['operational_course'];
$stepReturned = (bool)$cb['slingshot_returned'];
$cancelled = $cb['status'] === 'cancelled';
$sqStatus = $cb['square_payment_status'] ?? '';
$sqId = $cb['square_payment_id'] ?? '';
$sqCardId = $cb['square_card_id'] ?? '';
$cardLast4 = $cb['card_last4'] ?? '';
$cardBrand = $cb['card_brand'] ?? '';
?>
<div class="cust-booking-card">
<!-- Card header: ref, package, date, amount, status -->
<div class="cbc-header">
<span class="ci-ref" style="flex-shrink:0"><?= htmlspecialchars($cb['booking_ref']) ?></span>
<span style="font-size:.84rem;font-weight:600;flex:1;min-width:0">
<?= htmlspecialchars($pkg['label']) ?> &middot; <?= date('M j, Y', strtotime($cb['rental_date'])) ?> &middot; $<?= number_format($cb['amount'],2) ?>
</span>
<select class="status-sel" data-id="<?= $bid ?>" onchange="updateStatus(this)">
<?php foreach (['pending','confirmed','completed','cancelled'] as $s): ?>
<option value="<?= $s ?>" <?= $cb['status']===$s?'selected':'' ?>><?= ucfirst($s) ?></option>
<?php endforeach; ?>
</select>
</div>
<!-- Booking flow -->
<div class="cbc-flow">
<div class="flow-list">
<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($cb['created_at'])) ?></span>
</div>
</div>
<div class="flow-step">
<div class="flow-icon <?= $stepConfirmed?'done':($cancelled?'skip':'pending') ?>" id="cv-icon-<?= $bid ?>-confirmed">
<?= $stepConfirmed?'✓':($cancelled?'—':'2') ?>
</div>
<div class="flow-body">
<span class="flow-label">Booking Confirmed</span>
<span class="flow-meta" id="cv-meta-<?= $bid ?>-confirmed"><?= $stepConfirmed?('Confirmed — '.ucfirst($cb['status'])):($cancelled?'Cancelled':'Awaiting confirmation — change status above') ?></span>
</div>
</div>
<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<?= $cb['waiver_signed_at']?' on '.date('M j g:ia',strtotime($cb['waiver_signed_at'])):''; ?>
<?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="<?= SITE_URL ?>/waiver.php?ref=<?= urlencode($cb['booking_ref']) ?>" target="_blank">Open Waiver Link ↗</a>
</div>
<?php endif; ?>
</div>
</div>
<div class="flow-step">
<div class="flow-icon <?= $stepInsurance?'done':($cancelled?'skip':'pending') ?>" id="cv-icon-<?= $bid ?>-insurance_verified">
<?= $stepInsurance?'✓':($cancelled?'—':'4') ?>
</div>
<div class="flow-body">
<span class="flow-label">Insurance Received</span>
<span class="flow-meta" id="cv-meta-<?= $bid ?>-insurance_verified">
<?= $stepInsurance?'Verified — on file':($cancelled?'N/A':($insFile?'Doc submitted — verify at pickup':'Pending — verify at pickup')) ?>
</span>
<?php if (!$cancelled): ?>
<div class="flow-action" style="display:flex;flex-wrap:wrap;gap:6px;align-items:center;margin-top:4px">
<?php if ($insFile): ?>
<a class="flow-toggle" href="/view-doc.php?ref=<?= urlencode($cb['booking_ref']) ?>&type=insurance&_t=<?= $token ?>" target="_blank" style="text-decoration:none">📄 View Doc ↗</a>
<?php endif; ?>
<button class="flow-toggle <?= $stepInsurance?'active':'' ?>" id="cv-btn-<?= $bid ?>-insurance_verified"
onclick="cvToggleReq(<?= $bid ?>,'insurance_verified',this)">
<?= $stepInsurance?'✓ Verified at Pickup':'Mark Verified at Pickup' ?>
</button>
<button class="flow-toggle" onclick="openUploadLink('<?= htmlspecialchars($cb['booking_ref']) ?>','insurance')">📎 Upload Link</button>
</div>
<?php endif; ?>
</div>
</div>
<div class="flow-step">
<div class="flow-icon <?= $stepLicense?'done':($cancelled?'skip':'pending') ?>" id="cv-icon-<?= $bid ?>-license_verified">
<?= $stepLicense?'✓':($cancelled?'—':'5') ?>
</div>
<div class="flow-body">
<span class="flow-label">Driver's License Verified</span>
<span class="flow-meta" id="cv-meta-<?= $bid ?>-license_verified">
<?= $stepLicense?'Verified at pickup':($cancelled?'N/A':($licFile?'Doc submitted — verify at pickup':'Verify at pickup')) ?>
</span>
<?php if (!$cancelled): ?>
<div class="flow-action" style="display:flex;flex-wrap:wrap;gap:6px;align-items:center;margin-top:4px">
<?php if ($licFile): ?>
<a class="flow-toggle" href="/view-doc.php?ref=<?= urlencode($cb['booking_ref']) ?>&type=license&_t=<?= $token ?>" target="_blank" style="text-decoration:none">📄 View Doc ↗</a>
<?php endif; ?>
<button class="flow-toggle <?= $stepLicense?'active':'' ?>" id="cv-btn-<?= $bid ?>-license_verified"
onclick="cvToggleReq(<?= $bid ?>,'license_verified',this)">
<?= $stepLicense?'✓ Verified at Pickup':'Mark Verified at Pickup' ?>
</button>
<button class="flow-toggle" onclick="openUploadLink('<?= htmlspecialchars($cb['booking_ref']) ?>','license')">📎 Upload Link</button>
</div>
<?php endif; ?>
</div>
</div>
<?php
$cvDepIcon = $cancelled ? 'skip' : (($stepDeposit||$sqStatus==='COMPLETED') ? 'done' : 'pending');
$cvDepLabel = $cancelled ? '—' : (($stepDeposit||$sqStatus==='COMPLETED') ? '✓' : '6');
?>
<div class="flow-step">
<div class="flow-icon <?= $cvDepIcon ?>" id="cv-icon-<?= $bid ?>-deposit_received">
<?= $cvDepLabel ?>
</div>
<div class="flow-body">
<span class="flow-label">Deposit &amp; Balance — $<?= number_format(DEPOSIT_AMOUNT,2) ?> held &middot; $<?= number_format($cb['amount']-DEPOSIT_AMOUNT,2) ?> at pickup</span>
<span class="flow-meta" id="cv-meta-<?= $bid ?>-deposit_received">
<?php if ($cancelled): ?>N/A
<?php elseif ($sqStatus==='COMPLETED'): ?>Captured — $<?= number_format((float)($cb['deposit_paid']??DEPOSIT_AMOUNT),2) ?> charged
<?php elseif ($sqStatus==='REFUNDED'): ?>Refunded — deposit returned
<?php elseif ($sqStatus==='CANCELED'): ?>Hold voided — no charge
<?php elseif ($sqStatus==='APPROVED'||$sqStatus==='PENDING'): ?>Hold active — card authorized, not yet charged
<?php elseif ($stepDeposit): ?>Marked received (manual)
<?php else: ?>Pending — no card on file
<?php endif; ?>
</span>
<?php if (!$cancelled): ?>
<div class="flow-action" id="cv-dep-actions-<?= $bid ?>">
<?php if ($sqStatus==='APPROVED'||$sqStatus==='PENDING'): ?>
<button class="flow-toggle" onclick="cvSquareAction(<?= $bid ?>,'square_capture',this)" style="margin-right:4px">Capture $<?= number_format(DEPOSIT_AMOUNT,0) ?></button>
<button class="flow-toggle" onclick="cvSquareAction(<?= $bid ?>,'square_void',this)" style="border-color:#dc2626;color:#dc2626">Void &amp; Cancel Booking</button>
<?php elseif ($sqStatus==='COMPLETED'): ?>
<button class="flow-toggle" onclick="cvSquareAction(<?= $bid ?>,'square_refund',this)" style="border-color:#2563eb;color:#2563eb">Refund &amp; Cancel Booking</button>
<?php elseif (!$sqId): ?>
<button class="flow-toggle <?= $stepDeposit?'active':'' ?>" id="cv-btn-<?= $bid ?>-deposit_received"
onclick="cvToggleReq(<?= $bid ?>,'deposit_received',this)">
<?= $stepDeposit?'✓ Deposit Received':'Mark Deposit Received' ?>
</button>
<?php endif; ?>
</div>
<?php endif; ?>
</div>
</div>
<?php if (!$cancelled): ?>
<div style="margin:.4rem 0 .1rem;padding:.3rem .5rem;background:#fff7ed;border-radius:5px;font-size:.68rem;font-weight:700;text-transform:uppercase;letter-spacing:.5px;color:#92400e">Pre-Departure</div>
<div class="flow-step">
<div class="flow-icon <?= $stepHelmet?'done':'pending' ?>" id="cv-icon-<?= $bid ?>-helmet_provided">
<?= $stepHelmet?'✓':'7' ?>
</div>
<div class="flow-body">
<span class="flow-label">DOT Helmet Provided &amp; Fits</span>
<span class="flow-meta" id="cv-meta-<?= $bid ?>-helmet_provided"><?= $stepHelmet?'Helmet provided':'Pending' ?></span>
<div class="flow-action">
<button class="flow-toggle <?= $stepHelmet?'active':'' ?>" id="cv-btn-<?= $bid ?>-helmet_provided"
onclick="cvToggleReq(<?= $bid ?>,'helmet_provided',this)">
<?= $stepHelmet?'✓ Helmet Provided':'Mark Helmet Provided' ?>
</button>
</div>
</div>
</div>
<div class="flow-step">
<div class="flow-icon <?= $stepSafety?'done':'pending' ?>" id="cv-icon-<?= $bid ?>-safety_course">
<?= $stepSafety?'✓':'8' ?>
</div>
<div class="flow-body">
<span class="flow-label">Safety Course Completed</span>
<span class="flow-meta" id="cv-meta-<?= $bid ?>-safety_course"><?= $stepSafety?'Done':'Pending' ?></span>
<div class="flow-action">
<button class="flow-toggle <?= $stepSafety?'active':'' ?>" id="cv-btn-<?= $bid ?>-safety_course"
onclick="cvToggleReq(<?= $bid ?>,'safety_course',this)">
<?= $stepSafety?'✓ Safety Course Done':'Mark Safety Course Done' ?>
</button>
</div>
</div>
</div>
<div class="flow-step">
<div class="flow-icon <?= $stepOps?'done':'pending' ?>" id="cv-icon-<?= $bid ?>-operational_course">
<?= $stepOps?'✓':'9' ?>
</div>
<div class="flow-body">
<span class="flow-label">Operational Course Done</span>
<span class="flow-meta" id="cv-meta-<?= $bid ?>-operational_course"><?= $stepOps?'Done':'Pending' ?></span>
<div class="flow-action">
<button class="flow-toggle <?= $stepOps?'active':'' ?>" id="cv-btn-<?= $bid ?>-operational_course"
onclick="cvToggleReq(<?= $bid ?>,'operational_course',this)">
<?= $stepOps?'✓ Ops Course Done':'Mark Ops Course Done' ?>
</button>
</div>
</div>
</div>
<div class="flow-step">
<div class="flow-icon <?= $stepReturned?'done':'pending' ?>" id="cv-icon-<?= $bid ?>-slingshot_returned">
<?= $stepReturned?'✓':'10' ?>
</div>
<div class="flow-body">
<span class="flow-label">Slingshot Returned</span>
<span class="flow-meta" id="cv-meta-<?= $bid ?>-slingshot_returned"><?= $stepReturned?'Returned — booking closed':'Mark when returned to close out &amp; wipe card' ?></span>
<?php if (!$stepReturned): ?>
<div class="flow-action">
<button class="flow-toggle" style="border-color:#16a34a;color:#16a34a" onclick="markReturned(<?= $bid ?>,this)">✓ Mark Returned</button>
</div>
<?php endif; ?>
</div>
</div>
<?php endif; ?>
</div><!-- /.flow-list -->
</div><!-- /.cbc-flow -->
<!-- Admin notes -->
<div class="cbc-notes">
<div class="ci-field">Admin Notes</div>
<textarea class="notes-ta" id="cv-notes-<?= $bid ?>"><?= htmlspecialchars($cb['admin_notes']??'') ?></textarea>
<button class="save-btn" onclick="cvSaveNotes(<?= $bid ?>)" style="margin-top:.4rem">Save Notes</button>
</div>
</div><!-- /.cust-booking-card -->
<?php endforeach; ?>
<?php endif; ?>
</div><!-- /.cdp-bookings -->
</div><!-- /.cust-detail-panel -->
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</div>
</div><!-- /.main -->
<script>
const ADMIN_TOKEN = '<?= htmlspecialchars($token) ?>';
// ── Status filter (client-side — no page navigation) ─────────────────────────
function filterBookings(status, btn) {
document.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('active'));
if (btn) btn.classList.add('active');
const rows = document.querySelectorAll('tr.booking-row, tr.detail-row');
rows.forEach(row => {
const show = status === 'all' || row.dataset.status === status;
row.style.display = show ? '' : 'none';
});
}
// ── 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) {
const id = sel.dataset.id;
const status = sel.value;
fetch('/admin/', {
method:'POST',
headers:{'Content-Type':'application/x-www-form-urlencoded','X-Requested-With':'XMLHttpRequest'},
body:'action=update_status&id='+id+'&status='+status+'&_t='+ADMIN_TOKEN
}).then(r=>r.json()).then(d=>{
if (!d.ok) { alert('Error saving status'); return; }
// Update the "Booking Confirmed" step icon + meta in the detail panel
const confirmed = ['confirmed','completed'].includes(status);
const cancelled = status === 'cancelled';
['icon','cv-icon'].forEach(prefix => {
const icon = document.getElementById(prefix+'-'+id+'-confirmed');
if (!icon) return;
icon.className = 'flow-icon ' + (confirmed ? 'done' : (cancelled ? 'skip' : 'pending'));
icon.textContent = confirmed ? '✓' : (cancelled ? '—' : '2');
});
['meta','cv-meta'].forEach(prefix => {
const meta = document.getElementById(prefix+'-'+id+'-confirmed');
if (!meta) return;
meta.textContent = confirmed ? ('Confirmed — ' + status.charAt(0).toUpperCase()+status.slice(1))
: (cancelled ? 'Cancelled' : 'Awaiting confirmation — change status above');
});
// Update the data-status attribute on booking/detail rows so filters work
document.querySelectorAll('[data-status]').forEach(el => {
if (el.querySelector && el.querySelector('[data-id="'+id+'"]')) el.dataset.status = status;
});
document.querySelectorAll('tr.booking-row, tr.detail-row').forEach(row => {
const sel2 = row.querySelector('select[data-id="'+id+'"]');
if (sel2) row.dataset.status = 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+'&_t='+ADMIN_TOKEN
})
.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'],
helmet_provided: ['Mark Helmet Provided','✓ Helmet Provided'],
safety_course: ['Mark Safety Course Done','✓ Safety Course Done'],
operational_course: ['Mark Ops Course Done','✓ Ops Course Done'],
};
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');
const stepNums = {insurance_verified:'4', license_verified:'5', deposit_received:'6', helmet_provided:'7', safety_course:'8', operational_course:'9'};
icon.textContent = on ? '✓' : (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'],
helmet_provided: ['Provide DOT helmet — verify fit before departure','Helmet provided and fitted'],
safety_course: ['Complete safety briefing before departure','Safety course completed'],
operational_course: ['Walk customer through Slingshot controls before departure','Operational course completed'],
};
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');
// Also mirror to cv elements
const cvIcon = document.getElementById('cv-icon-'+id+'-'+field);
if (cvIcon) { cvIcon.className='flow-icon '+(on?'done':'pending'); cvIcon.textContent=on?'✓':(icon?icon.textContent:'?'); }
const cvMeta = document.getElementById('cv-meta-'+id+'-'+field);
if (cvMeta && metaTexts[field]) cvMeta.textContent = metaTexts[field][on?1:0];
const cvBtn = document.getElementById('cv-btn-'+id+'-'+field);
if (cvBtn) { cvBtn.classList.toggle('active',on); cvBtn.textContent=on?labels[field][1]:labels[field][0]; }
// 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)+'&_t='+ADMIN_TOKEN
}).then(r=>r.json()).then(d=>{
btn.textContent = d.ok ? 'Saved ✓' : 'Error';
setTimeout(()=>btn.textContent='Save Notes', 1800);
});
}
// ── Send reminder email ───────────────────────────────────────────────────────
function openUploadLink(ref, type) {
const url = 'https://parkerslingshot.epictravelexpeditions.com/upload-docs.php?ref=' + encodeURIComponent(ref) + '&type=' + type;
window.open(url, '_blank');
}
function resendConfirmation(id, btn) {
const emailInput = document.getElementById('resend-email-' + id);
const status = document.getElementById('resend-status-' + id);
const email = emailInput ? emailInput.value.trim() : '';
if (!email) { alert('Enter an email address.'); return; }
if (!confirm('Resend booking confirmation to ' + email + '?')) 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=resend_confirmation&id=' + id + '&email=' + encodeURIComponent(email) + '&_t=' + ADMIN_TOKEN
}).then(r => r.json()).then(d => {
btn.disabled = false;
btn.textContent = 'Resend';
if (d.ok) {
status.textContent = '✓ Sent to ' + d.email;
status.style.color = '#16a34a';
} else {
status.textContent = '✗ ' + (d.error || 'Failed');
status.style.color = '#dc2626';
}
status.style.display = 'block';
setTimeout(() => status.style.display = 'none', 4000);
}).catch(() => { btn.disabled = false; btn.textContent = 'Resend'; alert('Request failed.'); });
}
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(',')+'&_t='+ADMIN_TOKEN
}).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 $<?= number_format(DEPOSIT_AMOUNT,2) ?> deposit hold to this card?',
square_void: 'Void the $<?= number_format(DEPOSIT_AMOUNT,2) ?> deposit hold and CANCEL this booking? The customer will NOT be charged.',
square_refund: 'Refund the $<?= number_format(DEPOSIT_AMOUNT,2) ?> deposit and CANCEL this booking? This cannot be undone.',
}[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+'&_t='+ADMIN_TOKEN
}).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 &amp; Cancel Booking</button>';
} else if (d.status === 'CANCELED' || d.status === 'REFUNDED') {
const t = d.status === 'CANCELED' ? 'Hold voided — booking cancelled' : 'Refunded — booking cancelled';
if (meta) meta.textContent = t;
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 = '';
// Update booking status selects and row data-status
document.querySelectorAll('select.status-sel[data-id="'+id+'"]').forEach(sel => {
sel.value = 'cancelled';
});
document.querySelectorAll('tr.booking-row, tr.detail-row').forEach(row => {
const s = row.querySelector('select[data-id="'+id+'"]');
if (s) row.dataset.status = 'cancelled';
});
// Update confirmed step icon
['icon','cv-icon'].forEach(p => {
const el = document.getElementById(p+'-'+id+'-confirmed');
if (el) { el.className='flow-icon skip'; el.textContent='—'; }
});
['meta','cv-meta'].forEach(p => {
const el = document.getElementById(p+'-'+id+'-confirmed');
if (el) el.textContent = 'Cancelled';
});
}
} 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.');
});
}
// ── Customer CRUD ─────────────────────────────────────────────────────────────
function toggleCustForm(id) {
const form = document.getElementById('custForm');
const isOpen = form.classList.contains('open');
if (isOpen && id === 0) {
form.classList.remove('open');
return;
}
if (!isOpen || id === 0) {
// Reset form for new customer
document.getElementById('custId').value = 0;
document.getElementById('custName').value = '';
document.getElementById('custEmail').value = '';
document.getElementById('custPhone').value = '';
document.getElementById('custDob').value = '';
document.getElementById('custAddr').value = '';
document.getElementById('custNotes').value = '';
const msg = document.getElementById('custFormMsg');
msg.style.display = 'none';
document.getElementById('addCustBtn').textContent = '✕ Close';
}
form.classList.add('open');
document.getElementById('custName').focus();
}
function editCustomer(c) {
document.getElementById('custId').value = c.id;
document.getElementById('custName').value = c.name || '';
document.getElementById('custEmail').value = c.email || '';
document.getElementById('custPhone').value = c.phone || '';
document.getElementById('custDob').value = c.dob || '';
document.getElementById('custAddr').value = c.address || '';
document.getElementById('custNotes').value = c.notes || '';
const msg = document.getElementById('custFormMsg');
msg.style.display = 'none';
const form = document.getElementById('custForm');
form.classList.add('open');
document.getElementById('addCustBtn').textContent = '✕ Close';
form.scrollIntoView({behavior:'smooth', block:'nearest'});
}
function saveCustomer() {
const id = document.getElementById('custId').value;
const name = document.getElementById('custName').value.trim();
const email = document.getElementById('custEmail').value.trim();
const msg = document.getElementById('custFormMsg');
if (!name || !email) { showCustMsg('Name and email are required.', 'red'); return; }
const body = 'action=customer_save'
+ '&id=' + encodeURIComponent(id)
+ '&name=' + encodeURIComponent(name)
+ '&email=' + encodeURIComponent(email)
+ '&phone=' + encodeURIComponent(document.getElementById('custPhone').value)
+ '&dob=' + encodeURIComponent(document.getElementById('custDob').value)
+ '&address=' + encodeURIComponent(document.getElementById('custAddr').value)
+ '&notes=' + encodeURIComponent(document.getElementById('custNotes').value)
+ '&_t=' + ADMIN_TOKEN;
fetch('/admin/', {
method:'POST',
headers:{'Content-Type':'application/x-www-form-urlencoded','X-Requested-With':'XMLHttpRequest'},
body
}).then(r=>r.json()).then(d=>{
if (d.ok) {
showCustMsg(id === '0' ? 'Customer added.' : 'Saved.', '#16a34a');
setTimeout(()=>{ location.href = '/admin/?_t=' + ADMIN_TOKEN; }, 900);
} else {
showCustMsg(d.error || 'Error saving.', 'red');
}
}).catch(()=>showCustMsg('Request failed.', 'red'));
}
function deleteCustomer(id, btn) {
if (!confirm('Delete this customer? This cannot be undone.')) return;
btn.disabled = true;
fetch('/admin/', {
method:'POST',
headers:{'Content-Type':'application/x-www-form-urlencoded','X-Requested-With':'XMLHttpRequest'},
body:'action=customer_delete&id='+id+'&_t='+ADMIN_TOKEN
}).then(r=>r.json()).then(d=>{
if (d.ok) btn.closest('tr').remove();
else { btn.disabled=false; alert(d.error||'Error deleting.'); }
});
}
function filterCustomers(q) {
const term = q.toLowerCase().trim();
document.querySelectorAll('tr.cust-row').forEach(row => {
const show = !term || row.dataset.search.includes(term);
row.style.display = show ? '' : 'none';
const detail = document.getElementById('cdetail-' + row.dataset.cid);
if (detail && !show) detail.style.display = 'none';
});
}
function toggleCustDetail(cid) {
const row = document.getElementById('cdetail-' + cid);
const btn = document.getElementById('cexpand-' + cid);
const open = row.style.display !== 'table-row';
row.style.display = open ? 'table-row' : 'none';
btn.classList.toggle('open', open);
}
function cvToggleReq(bid, 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='+bid+'&field='+field+'&_t='+ADMIN_TOKEN
}).then(r=>r.json()).then(d=>{
if (!d.ok) { alert('Error saving'); btn.disabled=false; return; }
const on = d.value === 1;
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'],
helmet_provided: ['Mark Helmet Provided','✓ Helmet Provided'],
safety_course: ['Mark Safety Course Done','✓ Safety Course Done'],
operational_course: ['Mark Ops Course Done','✓ Ops Course Done'],
};
btn.textContent = on ? labels[field][1] : labels[field][0];
btn.disabled = false;
const stepNums = {insurance_verified:'4', license_verified:'5', deposit_received:'6', helmet_provided:'7', safety_course:'8', operational_course:'9'};
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'],
helmet_provided: ['Provide DOT helmet — verify fit before departure','Helmet provided and fitted'],
safety_course: ['Complete safety briefing before departure','Safety course completed'],
operational_course: ['Walk customer through Slingshot controls before departure','Operational course completed'],
};
const cvIcon = document.getElementById('cv-icon-'+bid+'-'+field);
if (cvIcon) { cvIcon.className='flow-icon '+(on?'done':'pending'); cvIcon.textContent=on?'✓':stepNums[field]; }
const cvMeta = document.getElementById('cv-meta-'+bid+'-'+field);
if (cvMeta && metaTexts[field]) cvMeta.textContent = metaTexts[field][on?1:0];
const dot = document.getElementById('dot-'+bid+'-'+field);
if (dot) dot.className = 'dot '+(on?'dot-done':'dot-pending');
const icon = document.getElementById('icon-'+bid+'-'+field);
if (icon) { icon.className='flow-icon '+(on?'done':'pending'); icon.textContent=on?'✓':stepNums[field]; }
const meta = document.getElementById('meta-'+bid+'-'+field);
if (meta && metaTexts[field]) meta.textContent = metaTexts[field][on?1:0];
const mainBtn = document.getElementById('btn-'+bid+'-'+field);
if (mainBtn) { mainBtn.classList.toggle('active',on); mainBtn.textContent=on?labels[field][1]:labels[field][0]; }
});
}
function cvSquareAction(bid, action, btn) {
const labels = {
square_capture: ['Capture','Capturing…','Captured ✓'],
square_void: ['Void Hold','Voiding…','Voided ✓'],
square_refund: ['Refund','Refunding…','Refunded ✓'],
};
const [orig, working] = labels[action];
const confirmMsg = {
square_capture: 'Charge the $<?= number_format(DEPOSIT_AMOUNT,2) ?> deposit hold to this card?',
square_void: 'Void the $<?= number_format(DEPOSIT_AMOUNT,2) ?> deposit hold and CANCEL this booking? The customer will NOT be charged.',
square_refund: 'Refund the $<?= number_format(DEPOSIT_AMOUNT,2) ?> deposit and CANCEL this booking? This cannot be undone.',
}[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='+bid+'&_t='+ADMIN_TOKEN
}).then(r=>r.json()).then(d=>{
if (d.ok) {
const cvMeta = document.getElementById('cv-meta-'+bid+'-deposit_received');
const cvIcon = document.getElementById('cv-icon-'+bid+'-deposit_received');
const cvArea = document.getElementById('cv-dep-actions-'+bid);
const meta = document.getElementById('meta-'+bid+'-deposit_received');
const icon = document.getElementById('icon-'+bid+'-deposit_received');
const dot = document.getElementById('dot-'+bid+'-deposit_received');
const area = document.getElementById('deposit-actions-'+bid);
if (d.status === 'COMPLETED') {
const t = 'Captured — deposit charged';
if (cvMeta) cvMeta.textContent = t;
if (cvIcon) { cvIcon.className='flow-icon done'; cvIcon.textContent='✓'; }
if (cvArea) cvArea.innerHTML = '<button class="flow-toggle" onclick="cvSquareAction('+bid+',\'square_refund\',this)" style="border-color:#2563eb;color:#2563eb">Refund &amp; Cancel Booking</button>';
if (meta) meta.textContent = t;
if (icon) { icon.className='flow-icon done'; icon.textContent='✓'; }
if (dot) dot.className='dot dot-done';
if (area) area.innerHTML = '<button class="flow-toggle" onclick="squareAction('+bid+',\'square_refund\',this)" style="border-color:#2563eb;color:#2563eb">Refund &amp; Cancel Booking</button>';
} else if (d.status === 'CANCELED' || d.status === 'REFUNDED') {
const t = d.status === 'CANCELED' ? 'Hold voided — booking cancelled' : 'Refunded — booking cancelled';
if (cvMeta) cvMeta.textContent = t;
if (cvIcon) { cvIcon.className='flow-icon skip'; cvIcon.textContent='—'; }
if (cvArea) cvArea.innerHTML = '';
if (meta) meta.textContent = t;
if (icon) { icon.className='flow-icon skip'; icon.textContent='—'; }
if (dot) dot.className='dot dot-skip';
if (area) area.innerHTML = '';
// Update booking status selects and row data-status
document.querySelectorAll('select.status-sel[data-id="'+bid+'"]').forEach(sel => {
sel.value = 'cancelled';
});
document.querySelectorAll('tr.booking-row, tr.detail-row').forEach(row => {
const s = row.querySelector('select[data-id="'+bid+'"]');
if (s) row.dataset.status = 'cancelled';
});
// Update confirmed step icon
['icon','cv-icon'].forEach(p => {
const el = document.getElementById(p+'-'+bid+'-confirmed');
if (el) { el.className='flow-icon skip'; el.textContent='—'; }
});
['meta','cv-meta'].forEach(p => {
const el = document.getElementById(p+'-'+bid+'-confirmed');
if (el) el.textContent = 'Cancelled';
});
}
} else {
btn.textContent = orig;
btn.disabled = false;
alert('Error: ' + (d.error||'Unknown error'));
}
}).catch(()=>{ btn.textContent=orig; btn.disabled=false; alert('Request failed.'); });
}
function cvSaveNotes(bid) {
const ta = document.getElementById('cv-notes-' + bid);
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='+bid+'&notes='+encodeURIComponent(ta.value)+'&_t='+ADMIN_TOKEN
}).then(r=>r.json()).then(d=>{
btn.textContent = d.ok ? 'Saved ✓' : 'Error';
setTimeout(()=>btn.textContent='Save Notes', 1800);
// Mirror to main booking table notes
const mainTa = document.getElementById('notes-'+bid);
if (mainTa) mainTa.value = ta.value;
});
}
function showCustMsg(text, color) {
const msg = document.getElementById('custFormMsg');
msg.textContent = text;
msg.style.color = color;
msg.style.display = 'block';
}
// ── Block / unblock dates ─────────────────────────────────────────────────────
function blockDate(e) {
e.preventDefault();
var dateVal = document.getElementById('blockDateInput').value;
var reason = document.getElementById('blockReasonInput').value;
fetch('/admin/', {
method:'POST',
headers:{'Content-Type':'application/x-www-form-urlencoded','X-Requested-With':'XMLHttpRequest'},
body:'action=block_date&date='+dateVal+'&reason='+encodeURIComponent(reason)+'&_t='+ADMIN_TOKEN
}).then(r=>r.json()).then(d=>{
if (d.ok) {
var list = document.querySelector('.block-list');
var empty = list.querySelector('p');
if (empty) empty.remove();
var dt = new Date(dateVal + 'T12:00:00');
var label = dt.toLocaleDateString('en-US',{month:'short',day:'numeric',year:'numeric'});
var item = document.createElement('div');
item.className = 'block-item';
item.id = 'blocked-' + d.id;
item.innerHTML = '<button class="del-btn" onclick="unblockDate('+d.id+')" title="Remove">✕</button>'
+ '<strong>' + label + '</strong>'
+ (d.reason ? '<span style="color:#6b7280"> — ' + d.reason.replace(/</g,'&lt;') + '</span>' : '');
list.appendChild(item);
document.getElementById('blockDateInput').value = '';
document.getElementById('blockReasonInput').value = '';
} 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+'&_t='+ADMIN_TOKEN
}).then(r=>r.json()).then(d=>{ if(d.ok) document.getElementById('blocked-'+id).remove(); });
}
// ── Mark slingshot returned ───────────────────────────────────────────────────
function markReturned(id, btn) {
if (!confirm('Mark this slingshot as returned? This will:\n• Set booking status to Completed\n• Wipe stored card data\n\nThis cannot be undone.')) return;
btn.disabled = true;
btn.textContent = 'Processing…';
fetch('/admin/', {
method:'POST',
headers:{'Content-Type':'application/x-www-form-urlencoded','X-Requested-With':'XMLHttpRequest'},
body:'action=mark_returned&id='+id+'&_t='+ADMIN_TOKEN
}).then(r=>r.json()).then(d=>{
if (d.ok) {
btn.textContent = '✓ Returned';
btn.style.background = '#16a34a'; btn.style.color = '#fff'; btn.style.border = 'none';
// Update icon + meta
['','cv-'].forEach(p => {
const icon = document.getElementById(p+'icon-'+id+'-slingshot_returned');
if (icon) { icon.className='flow-icon done'; icon.textContent='✓'; }
const meta = document.getElementById(p+'meta-'+id+'-slingshot_returned');
if (meta) meta.textContent = 'Returned — booking closed, card data wiped';
});
const dot = document.getElementById('dot-'+id+'-slingshot_returned');
if (dot) dot.className = 'dot dot-done';
// Update status selects
document.querySelectorAll('select[data-id="'+id+'"]').forEach(s => {
s.value = 'completed';
s.closest('tr')?.setAttribute('data-status','completed');
});
} else {
btn.disabled = false;
btn.textContent = '✓ Mark Slingshot Returned';
alert(d.error || 'Failed to mark returned.');
}
}).catch(()=>{ btn.disabled=false; btn.textContent='✓ Mark Slingshot Returned'; alert('Request failed.'); });
}
// ── Charge balance to stored card ─────────────────────────────────────────────
function chargeBalance(id, btn) {
const amt = btn.textContent.match(/\$[\d.]+/)?.[0] || 'the balance';
if (!confirm('Charge '+amt+' to the card on file?')) return;
btn.disabled = true;
btn.textContent = 'Charging…';
fetch('/admin/', {
method:'POST',
headers:{'Content-Type':'application/x-www-form-urlencoded','X-Requested-With':'XMLHttpRequest'},
body:'action=charge_balance&id='+id+'&_t='+ADMIN_TOKEN
}).then(r=>r.json()).then(d=>{
if (d.ok) {
btn.textContent = '✓ Balance Charged';
btn.style.background = '#16a34a'; btn.style.color = '#fff'; btn.style.border = 'none';
const meta = document.getElementById('meta-'+id+'-deposit_received');
if (meta) meta.textContent = 'Balance charged to card on file';
const dot = document.getElementById('dot-'+id+'-deposit_received');
if (dot) dot.className = 'dot dot-done';
} else {
btn.disabled = false;
btn.textContent = btn.getAttribute('data-orig') || 'Charge Balance';
alert('Error: ' + (d.error || 'Charge failed'));
}
}).catch(()=>{ btn.disabled=false; alert('Request failed.'); });
}
// ── Update card on file ───────────────────────────────────────────────────────
let _updateCardId = null;
let _sqPayments = null;
function openUpdateCard(id, triggerBtn) {
_updateCardId = id;
const modal = document.getElementById('updateCardModal');
if (!modal) return;
document.getElementById('ucMsg').textContent = '';
document.getElementById('ucMsg').style.display = 'none';
modal.style.display = 'flex';
// Init Square if not already
if (!_sqPayments) {
if (typeof Square === 'undefined') {
document.getElementById('ucMsg').textContent = 'Square SDK not loaded. Refresh the page.';
document.getElementById('ucMsg').style.display = 'block';
return;
}
Square.payments('<?= SQUARE_APP_ID ?>', '<?= SQUARE_LOCATION_ID ?>').then(payments => {
_sqPayments = payments;
return _sqPayments.card({
style: {
'.input-container': {
borderColor: '#d1d5db',
borderRadius: '6px',
},
'.input-container.is-focus': {
borderColor: '#f97316',
},
'.input-container.is-error': {
borderColor: '#dc2626',
},
'input': {
color: '#111111',
fontSize: '14px',
fontFamily: 'Inter, Arial, sans-serif',
},
'input::placeholder': {
color: '#9ca3af',
},
}
});
}).then(card => {
window._ucCard = card;
return card.attach('#ucCardContainer');
}).catch(e => {
document.getElementById('ucMsg').textContent = 'Could not load card form: ' + e.message;
document.getElementById('ucMsg').style.display = 'block';
});
}
}
function closeUpdateCard() {
document.getElementById('updateCardModal').style.display = 'none';
}
async function submitUpdateCard() {
const btn = document.getElementById('ucSubmitBtn');
const msg = document.getElementById('ucMsg');
if (!_sqPayments || !window._ucCard) { msg.textContent = 'Card form not ready.'; msg.style.display='block'; return; }
btn.disabled = true; btn.textContent = 'Processing…';
try {
const result = await window._ucCard.tokenize();
if (result.status !== 'OK') throw new Error(result.errors?.[0]?.message || 'Card tokenization failed');
const nonce = result.token;
const resp = await fetch('/admin/', {
method:'POST',
headers:{'Content-Type':'application/x-www-form-urlencoded','X-Requested-With':'XMLHttpRequest'},
body:'action=update_card&id='+_updateCardId+'&nonce='+encodeURIComponent(nonce)+'&_t='+ADMIN_TOKEN
});
const d = await resp.json();
if (d.ok) {
msg.textContent = '✓ Card updated — ' + (d.brand||'Card') + ' •••• ' + (d.last4||'');
msg.style.color = '#16a34a'; msg.style.display = 'block';
btn.textContent = 'Done';
setTimeout(closeUpdateCard, 2000);
} else {
throw new Error(d.error || 'Failed to save card');
}
} catch(e) {
msg.textContent = e.message;
msg.style.color = 'red'; msg.style.display = 'block';
btn.disabled = false; btn.textContent = 'Save New Card';
}
}
</script>
<!-- Update Card Modal -->
<div id="updateCardModal" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,.6);z-index:9999;align-items:center;justify-content:center">
<div style="background:#fff;border-radius:12px;padding:2rem;width:100%;max-width:420px;margin:1rem;color-scheme:light">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:1.25rem">
<h2 style="font-size:1rem;font-weight:700;margin:0">Update Card on File</h2>
<button onclick="closeUpdateCard()" style="background:none;border:none;font-size:1.3rem;cursor:pointer;color:#6b7280;line-height:1">✕</button>
</div>
<p style="font-size:.83rem;color:#6b7280;margin-bottom:1rem">Enter the customer's new card. The old card will be removed and replaced.</p>
<div id="ucCardContainer" style="border:1px solid #e5e7eb;border-radius:8px;padding:.75rem;min-height:54px;margin-bottom:1rem;background:#fff"></div>
<div id="ucMsg" style="display:none;font-size:.82rem;margin-bottom:.75rem;padding:.5rem .75rem;border-radius:6px;background:#fef2f2"></div>
<div style="display:flex;gap:.75rem">
<button id="ucSubmitBtn" onclick="submitUpdateCard()" style="flex:1;background:#f97316;color:#fff;border:none;border-radius:8px;padding:.75rem;font-size:.9rem;font-weight:700;cursor:pointer">Save New Card</button>
<button onclick="closeUpdateCard()" style="background:#f3f4f6;border:none;border-radius:8px;padding:.75rem 1.25rem;font-size:.9rem;cursor:pointer;color:#374151">Cancel</button>
</div>
</div>
</div>
<script src="https://web.squarecdn.com/v1/square.js"></script>
</body>
</html>