mirror of
https://github.com/myronblair/parkerslingshot
synced 2026-06-30 17:50:22 -05:00
2257 lines
126 KiB
PHP
2257 lines
126 KiB
PHP
<?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 →</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 →</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 →</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'>© " . date('Y') . " Parker County Slingshot Rentals — 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 →</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'>© " . date('Y') . " Parker County Slingshot Rentals — 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 ?>)">►</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 & 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 & 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 1–5 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 & 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 1–5 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 & 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 & 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 ?>)">►</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']) ?> · <?= date('M j, Y', strtotime($cb['rental_date'])) ?> · $<?= 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 & Balance — $<?= number_format(DEPOSIT_AMOUNT,2) ?> held · $<?= 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 & Cancel Booking</button>
|
||
<?php elseif ($sqStatus==='COMPLETED'): ?>
|
||
<button class="flow-toggle" onclick="cvSquareAction(<?= $bid ?>,'square_refund',this)" style="border-color:#2563eb;color:#2563eb">Refund & 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 & 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 & 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+'¬es='+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 & 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)
|
||
+ '¬es=' + 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 & 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 & 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+'¬es='+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,'<') + '</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>
|