+
= $stepInsurance?'✓':($cancelled?'—':'4') ?>
Proof of Insurance Received
- = $stepInsurance?'Verified — on file':($cancelled?'N/A':'Pending — verify at pickup') ?>
+ = $stepInsurance?'Verified — on file':($cancelled?'N/A':($insFile?'Doc submitted — verify at pickup':'Pending — verify at pickup')) ?>
+
+
📄 View Submitted Doc ↗
+
-
+
- = $stepInsurance?'✓ Marked Received':'Mark Received' ?>
+ = $stepInsurance?'✓ Verified at Pickup':'Mark Verified at Pickup' ?>
+
+ 📎 Copy Upload Link
+
@@ -820,15 +883,21 @@ textarea.notes-ta:focus{border-color:#f97316}
Driver's License Verified
- = $stepLicense?'License verified':($cancelled?'N/A':'Verify at pickup') ?>
+ = $stepLicense?'Verified at pickup':($cancelled?'N/A':($licFile?'Doc submitted — verify at pickup':'Verify at pickup')) ?>
+
+
📄 View Submitted Doc ↗
+
-
+
- = $stepLicense?'✓ License Verified':'Mark License Verified' ?>
+ = $stepLicense?'✓ Verified at Pickup':'Mark Verified at Pickup' ?>
+
+ 📎 Copy Upload Link
+
@@ -1034,6 +1103,16 @@ textarea.notes-ta:focus{border-color:#f97316}
Waiver Link
https://parkerslingshot.epictravelexpeditions.com/waiver.php?ref== htmlspecialchars($b['booking_ref']) ?>
+
+
+
Resend Confirmation Email
+
+
+ Resend
+
+
+
@@ -1586,6 +1665,42 @@ function saveNotes(id) {
}
// ── Send reminder email ───────────────────────────────────────────────────────
+function copyUploadLink(ref, type, btn) {
+ const url = 'https://parkerslingshot.epictravelexpeditions.com/upload-docs.php?ref=' + encodeURIComponent(ref) + '&type=' + type;
+ navigator.clipboard.writeText(url).then(() => {
+ const orig = btn.textContent;
+ btn.textContent = '✓ Copied!';
+ setTimeout(() => btn.textContent = orig, 2000);
+ }).catch(() => prompt('Copy this link:', url));
+}
+
+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);
diff --git a/upload-docs.php b/upload-docs.php
new file mode 100644
index 0000000..6e0902f
--- /dev/null
+++ b/upload-docs.php
@@ -0,0 +1,189 @@
+prepare("SELECT id, name, email, booking_ref, rental_date, status FROM bookings WHERE booking_ref=?");
+ $stmt->execute([$ref]);
+ $booking = $stmt->fetch();
+ if (!$booking) $error = 'Booking not found. Please check your confirmation email.';
+ elseif ($booking['status'] === 'cancelled') $error = 'This booking has been cancelled.';
+}
+
+if ($_SERVER['REQUEST_METHOD'] === 'POST' && $booking && !$error) {
+ $file = $_FILES['doc'] ?? null;
+ if (!$file || $file['error'] !== UPLOAD_ERR_OK) {
+ $error = 'Upload failed — please try again or check file size.';
+ } else {
+ $finfo = new finfo(FILEINFO_MIME_TYPE);
+ $mime = $finfo->file($file['tmp_name']);
+ $allowed = ['image/jpeg','image/png','application/pdf'];
+ if (!in_array($mime, $allowed)) {
+ $error = 'Only JPG, PNG, or PDF files are accepted.';
+ } elseif ($file['size'] > 10 * 1024 * 1024) {
+ $error = 'File must be under 10 MB.';
+ } else {
+ $ext = ['image/jpeg'=>'jpg','image/png'=>'png','application/pdf'=>'pdf'][$mime];
+ $dir = __DIR__ . '/uploads/' . $ref;
+ if (!is_dir($dir)) mkdir($dir, 0750, true);
+ $fname = $type . '_' . date('YmdHis') . '.' . $ext;
+ $dest = $dir . '/' . $fname;
+ if (move_uploaded_file($file['tmp_name'], $dest)) {
+ $col = $type === 'license' ? 'license_file' : 'insurance_file';
+ $rel = 'uploads/' . $ref . '/' . $fname;
+ db()->prepare("UPDATE bookings SET {$col}=? WHERE booking_ref=?")->execute([$rel, $ref]);
+
+ $typeLabel = $type === 'license' ? "Driver's License" : 'Proof of Insurance';
+ $dateLabel = date('F j, Y', strtotime($booking['rental_date']));
+ $adminHtml = "
+
+
{$typeLabel} Uploaded — {$booking['booking_ref']}
+
+
+
" . htmlspecialchars($booking['name']) . " uploaded their {$typeLabel} for booking {$booking['booking_ref']} (rental: {$dateLabel}).
+
View it in the admin panel under their booking detail.
+
+
+
";
+ sendEmail(ADMIN_EMAIL, 'Parker Slingshot Admin', "{$typeLabel} Uploaded — {$booking['booking_ref']}: " . $booking['name'], $adminHtml);
+ $done = true;
+ } else {
+ $error = 'Could not save file. Please try again.';
+ }
+ }
+ }
+}
+
+$typeLabel = $type === 'license' ? "Driver's License" : ($type === 'insurance' ? 'Proof of Insurance' : '');
+$dateLabel = $booking ? date('F j, Y', strtotime($booking['rental_date'])) : '';
+?>
+
+
+
+
+
Upload Document — Parker County Slingshot Rentals
+
+
+
+
+
+
+
+
+
+
+
+
Upload Document
+
Invalid or missing upload link. Please use the link from your email or contact us.
+
+
+
+
+
= htmlspecialchars($error) ?>
+
Need help? Call or text (817) 555-0199 .
+
+
+
+
+
✅
+
+
Upload Received!
+
Thanks, = htmlspecialchars($booking['name']) ?>! Your = htmlspecialchars($typeLabel) ?> has been submitted for booking = htmlspecialchars($booking['booking_ref']) ?> .
+
We'll review it and still do a quick visual check at pickup. See you on = htmlspecialchars($dateLabel) ?>!
+
+
+
+
+
= htmlspecialchars($error) ?>
+
+
Upload = htmlspecialchars($typeLabel) ?>
+
+
= htmlspecialchars($booking['booking_ref']) ?>
+
= htmlspecialchars($booking['name']) ?> — = htmlspecialchars($dateLabel) ?>
+
+
+
+ Please upload a photo or scan of your current auto insurance card. JPG, PNG, or PDF accepted (max 10 MB).
+
+ Please upload a photo or scan of the front of your driver's license. JPG, PNG, or PDF accepted (max 10 MB).
+
+
+
We'll still do a visual check at pickup — this is just for our records.
+
+
Your document is stored securely and only visible to Parker County Slingshot Rentals staff.
+
+
+
+
+
+
+
diff --git a/view-doc.php/parker-admin.php b/view-doc.php/parker-admin.php
new file mode 100644
index 0000000..05cfcce
--- /dev/null
+++ b/view-doc.php/parker-admin.php
@@ -0,0 +1,2233 @@
+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' => "
",
+ ],
+ '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' => "
",
+ ],
+ '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' => "
",
+ ],
+ ];
+
+ $rowsHtml = '';
+ $n = 1;
+ foreach ($keys as $key) {
+ if (!isset($itemDefs[$key])) continue;
+ $d = $itemDefs[$key];
+ $rowsHtml .= "
+
+
+ {$n}
+
+
+ " . htmlspecialchars($d['label']) . "
+ " . htmlspecialchars($d['detail']) . "
+ {$d['cta']}
+
+ ";
+ $n++;
+ }
+
+ if (!$rowsHtml) { echo json_encode(['error'=>'No items selected']); exit; }
+
+ $html = "
+
+
+
Parker County Slingshot Rentals
+
+
+
Almost Ready — A Few Things Before Pickup
+
Hey " . htmlspecialchars($b['name']) . ", your " . htmlspecialchars($pkg['label']) . " rental on {$dateLabel} is coming up! (Ref: {$ref} )
+
To make sure pickup goes smoothly, here's what still needs to be taken care of:
+
+
+
Questions? Call or text (817) 555-0199 or reply to this email — we're happy to help.
+
+
Ride on,The Parker County Slingshot Team
+
+
+
© " . date('Y') . " Parker County Slingshot Rentals — Weatherford, TX
+
+
";
+
+ $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. Set MAILJET_API_KEY and MAILJET_SECRET_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 = "
+
+
Parker County Slingshot Rentals
+
+
+
Booking Confirmation
+
Hey " . htmlspecialchars($b['name']) . ", here's a copy of your booking confirmation.
+
+
Booking Reference
+
{$ref}
+
Package: " . htmlspecialchars($pkg['label']) . "
+
Date: {$dateLabel}
+
Total: {$amtLabel}
+
Deposit hold: {$depLabel}
+
Balance at pickup: {$balLabel}
+
+
+
Questions? Call or text " . ADMIN_PHONE . " or reply to this email.
+
Ride on,The Parker County Slingshot Team
+
+
+
© " . date('Y') . " Parker County Slingshot Rentals — Weatherford, TX
+
+
";
+ $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) { ?>
+
+
+
+
+
Admin Login — Parker County Slingshot Rentals
+
+
+
+
+
Parker Admin
+
Slingshot Rentals Management
+
+
Invalid username or password.
+
+
+
+
+
+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();
+?>
+
+
+
+
+
Admin — Parker County Slingshot Rentals
+
+
+
+
+ Parker County Slingshot — Admin
+ Sign Out
+
+
+
+
+
+
= (int)$stats['total'] ?>
Total Bookings
+
= (int)$stats['pending'] ?>
Pending
+
= (int)$stats['confirmed'] ?>
Confirmed
+
= (int)$stats['completed'] ?>
Completed
+
$= number_format((float)$stats['revenue'],0) ?>
Revenue
+
= (int)$stats['waivers_signed'] ?>
Waivers Signed
+
= (int)$stats['insurance_done'] ?>
Insurance OK
+
= (int)$stats['deposits_done'] ?>
Deposits Rcvd
+
+
+
+
+
+
+
+
No bookings found.
+
+
+
+
+
+
+ Customer
+ Rental Date
+ Package
+ Amount
+ Status
+ Progress
+ Submitted
+
+
+
+ $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)
+ ));
+ ?>
+
+
+ ►
+
+
+ = htmlspecialchars($b['name']) ?>
+ = htmlspecialchars($b['email']) ?>
+
+
+ = date('M j, Y', strtotime($b['rental_date'])) ?>
+
+ → = date('M j', strtotime($b['end_date'])) ?>
+
+
+ = htmlspecialchars($pkg['label']) ?>
+ $= number_format($b['amount'],2) ?>
+
+
+
+ >= ucfirst($s) ?>
+
+
+
+
+
+
+ = $pendingCount ?> pending
+
+ All done ✓
+
+
+ = date('M j g:ia', strtotime($b['created_at'])) ?>
+
+
+
+
+
+
+
+
+
+
Customer
+
= htmlspecialchars($b['booking_ref']) ?>
+
= htmlspecialchars($b['name']) ?>
+
+
+
+
+
Package
+
= htmlspecialchars($pkg['label']) ?>
+
$= number_format($b['amount'],2) ?>
+
Rental Date
+
= date('F j, Y', strtotime($b['rental_date'])) ?>
+
+
Customer Message
+
= nl2br(htmlspecialchars($b['notes'])) ?>
+
+
+
+
Admin Notes
+
+
Save Notes
+
+
+
+
+
+
Booking Flow
+
+
+
+
+
✓
+
+ Booking Submitted
+ = date('M j, Y g:ia', strtotime($b['created_at'])) ?>
+
+
+
+
+
+
+ = $stepConfirmed?'✓':($cancelled?'—':'2') ?>
+
+
+ Booking Confirmed
+
+ Confirmed — status: = ucfirst($b['status']) ?>
+ Cancelled
+ Awaiting confirmation — change status above
+
+
+
+
+
+
+
+
+ = $stepWaiver?'✓':($cancelled?'—':'3') ?>
+
+
+
Rental Waiver Signed
+
+
+ Signed by = htmlspecialchars($b['waiver_name'] ?? $b['name']) ?>
+ on = date('M j g:ia', strtotime($b['waiver_signed_at'])) ?>
+ N/A
+ Not yet signed
+
+
+
+
+
+
+
+
+
+
+
+ = $stepInsurance?'✓':($cancelled?'—':'4') ?>
+
+
+
Proof of Insurance Received
+
+ = $stepInsurance?'Verified — on file':($cancelled?'N/A':($insFile?'Doc submitted — verify at pickup':'Pending — verify at pickup')) ?>
+
+
+
📄 View Submitted Doc ↗
+
+
+
+
+ = $stepInsurance?'✓ Verified at Pickup':'Mark Verified at Pickup' ?>
+
+
+ 📎 Copy Upload Link
+
+
+
+
+
+
+
+
+
+ = $stepLicense?'✓':($cancelled?'—':'5') ?>
+
+
+
Driver's License Verified
+
+ = $stepLicense?'Verified at pickup':($cancelled?'N/A':($licFile?'Doc submitted — verify at pickup':'Verify at pickup')) ?>
+
+
+
📄 View Submitted Doc ↗
+
+
+
+
+ = $stepLicense?'✓ Verified at Pickup':'Mark Verified at Pickup' ?>
+
+
+ 📎 Copy Upload Link
+
+
+
+
+
+
+
+
+
+
+ = $depositLabel ?>
+
+
+
Deposit & Balance — $= number_format(DEPOSIT_AMOUNT,2) ?> held · $= number_format($b['amount']-DEPOSIT_AMOUNT,2) ?> at pickup
+
+
+ = htmlspecialchars($cardBrand) ?> •••• = htmlspecialchars($cardLast4) ?> on file
+ Update Card
+
+
+
+ N/A
+ Balance charged to card on file
+ Captured — $= number_format((float)($b['deposit_paid']??DEPOSIT_AMOUNT),2) ?> charged
+ Refunded — $= number_format((float)($b['deposit_paid']??DEPOSIT_AMOUNT),2) ?>
+ Hold voided — no charge
+ Hold active — card authorized, not yet charged
+ Card was declined — contact customer
+ Deposit marked received (manual)
+ Pending — no card on file yet
+
+
+
+
+
+ Capture $= number_format(DEPOSIT_AMOUNT,0) ?>
+ Void & Cancel Booking
+
+ Charge Balance $= number_format($b['amount']-DEPOSIT_AMOUNT,2) ?>
+
+ Charge Balance $= number_format($b['amount']-DEPOSIT_AMOUNT,2) ?>
+
+
+ Refund & Cancel Booking
+
+ Charge Balance $= number_format($b['amount']-DEPOSIT_AMOUNT,2) ?>
+
+ Charge Balance $= number_format($b['amount']-DEPOSIT_AMOUNT,2) ?>
+
+
+
+ = $stepDeposit?'✓ Deposit Received':'Mark Deposit Received' ?>
+
+
+
+
+
+
+
+
+
+
+ Pre-Departure Checklist
+
+
+
+
+
+ = $stepHelmet?'✓':'7' ?>
+
+
+
DOT Helmet Provided & Fits
+
+ = $stepHelmet?'Helmet provided and fitted':'Provide DOT helmet — verify fit before departure' ?>
+
+
+
+ = $stepHelmet?'✓ Helmet Provided':'Mark Helmet Provided' ?>
+
+
+
+
+
+
+
+
+ = $stepSafety?'✓':'8' ?>
+
+
+
Safety Course Completed
+
+ = $stepSafety?'Safety course completed':'Complete safety briefing before departure' ?>
+
+
+
+ = $stepSafety?'✓ Safety Course Done':'Mark Safety Course Done' ?>
+
+
+
+
+
+
+
+
+ = $stepOps?'✓':'9' ?>
+
+
+
Slingshot Operational Course Done
+
+ = $stepOps?'Operational course completed':'Walk customer through Slingshot controls before departure' ?>
+
+
+
+ = $stepOps?'✓ Ops Course Done':'Mark Ops Course Done' ?>
+
+
+
+
+
+
+
+
+ = $stepReturned?'✓':'10' ?>
+
+
+
Slingshot Returned — Close Out
+
+
+ Returned = $b['returned_at'] ? date('M j, Y g:ia', strtotime($b['returned_at'])) : '' ?> — booking closed, card data wiped
+
+ Mark when slingshot is safely returned — closes booking & wipes card data
+
+
+
+
+
+ ✓ Mark Slingshot Returned
+
+
+
+
+
+
+
+
+
+
+
+
+
Send Reminder Email
+
Select what the customer still needs to do, then send them a nudge email with clear instructions.
+
+
+
Not applicable for cancelled bookings.
+
+
+
+
+ Waiver Link
+ https://parkerslingshot.epictravelexpeditions.com/waiver.php?ref== htmlspecialchars($b['booking_ref']) ?>
+
+
+
+
Resend Confirmation Email
+
+
+ Resend
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
No dates blocked.
+
+
+
+ ✕
+ = date('M j, Y', strtotime($bl['block_date'])) ?>
+ — = htmlspecialchars($bl['reason']) ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
No customers yet.
+
+
+
+
+
+
+ Name
+ Email
+ Phone
+ Bookings
+ Status
+ Added
+
+
+
+
+
+
+
+ ►
+
+ = htmlspecialchars($c['name']) ?>
+ = htmlspecialchars($c['email']) ?>
+ = htmlspecialchars($c['phone'] ?? '—') ?>
+ = count($custBookings) ?>
+ = $c['is_active']?'Active':'Inactive' ?>
+ = date('M j, Y', strtotime($c['created_at'])) ?>
+
+ Edit
+ Del
+
+
+
+
+
+
+
+
+
+
+
Customer Profile
+
= htmlspecialchars($c['name']) ?>
+
+
+
+
+
+
Date of Birth
+
= date('F j, Y', strtotime($c['dob'])) ?>
+
+
+
Address
+
= nl2br(htmlspecialchars($c['address'])) ?>
+
+
+
Notes
+
= nl2br(htmlspecialchars($c['notes'])) ?>
+
+
+ = $c['is_active']?'Active':'Inactive' ?>
+ Edit Profile
+
+
+
+
+
+
Booking History (= count($custBookings) ?>)
+
+
No bookings yet.
+
+ $cb['package']];
+ $stepConfirmed = in_array($cb['status'], ['confirmed','completed']);
+ $stepWaiver = (bool)$cb['waiver_signed'];
+ $stepInsurance = (bool)$cb['insurance_verified'];
+ $stepDeposit = (bool)$cb['deposit_received'];
+ $stepLicense = (bool)$cb['license_verified'];
+ $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'] ?? '';
+ ?>
+
+
+
+
+
+
+
+
+
+
+
✓
+
+ Booking Submitted
+ = date('M j, Y g:ia', strtotime($cb['created_at'])) ?>
+
+
+
+
+
+ = $stepConfirmed?'✓':($cancelled?'—':'2') ?>
+
+
+ Booking Confirmed
+ = $stepConfirmed?('Confirmed — '.ucfirst($cb['status'])):($cancelled?'Cancelled':'Awaiting confirmation — change status above') ?>
+
+
+
+
+
+ = $stepWaiver?'✓':($cancelled?'—':'3') ?>
+
+
+
Rental Waiver Signed
+
+ Signed= $cb['waiver_signed_at']?' on '.date('M j g:ia',strtotime($cb['waiver_signed_at'])):''; ?>
+ N/A
+ Not yet signed
+
+
+
+
+
+
+
+
+
+ = $stepInsurance?'✓':($cancelled?'—':'4') ?>
+
+
+
Insurance Received
+
+ = $stepInsurance?'Verified — on file':($cancelled?'N/A':'Pending — verify at pickup') ?>
+
+
+
+
+ = $stepInsurance?'✓ Marked Received':'Mark Received' ?>
+
+
+
+
+
+
+
+
+ = $stepLicense?'✓':($cancelled?'—':'5') ?>
+
+
+
Driver's License Verified
+
+ = $stepLicense?'Verified — on file':($cancelled?'N/A':'Verify at pickup') ?>
+
+
+
+
+ = $stepLicense?'✓ License Verified':'Mark License Verified' ?>
+
+
+
+
+
+
+
+
+
+ = $cvDepLabel ?>
+
+
+
Deposit & Balance — $= number_format(DEPOSIT_AMOUNT,2) ?> held · $= number_format($cb['amount']-DEPOSIT_AMOUNT,2) ?> at pickup
+
+ N/A
+ Captured — $= number_format((float)($cb['deposit_paid']??DEPOSIT_AMOUNT),2) ?> charged
+ Refunded — deposit returned
+ Hold voided — no charge
+ Hold active — card authorized, not yet charged
+ Marked received (manual)
+ Pending — no card on file
+
+
+
+
+
+ Capture $= number_format(DEPOSIT_AMOUNT,0) ?>
+ Void & Cancel Booking
+
+ Refund & Cancel Booking
+
+
+ = $stepDeposit?'✓ Deposit Received':'Mark Deposit Received' ?>
+
+
+
+
+
+
+
+
+
Pre-Departure
+
+
+
+ = $stepHelmet?'✓':'7' ?>
+
+
+
DOT Helmet Provided & Fits
+
= $stepHelmet?'Helmet provided':'Pending' ?>
+
+
+ = $stepHelmet?'✓ Helmet Provided':'Mark Helmet Provided' ?>
+
+
+
+
+
+
+
+ = $stepSafety?'✓':'8' ?>
+
+
+
Safety Course Completed
+
= $stepSafety?'Done':'Pending' ?>
+
+
+ = $stepSafety?'✓ Safety Course Done':'Mark Safety Course Done' ?>
+
+
+
+
+
+
+
+ = $stepOps?'✓':'9' ?>
+
+
+
Operational Course Done
+
= $stepOps?'Done':'Pending' ?>
+
+
+ = $stepOps?'✓ Ops Course Done':'Mark Ops Course Done' ?>
+
+
+
+
+
+
+
+ = $stepReturned?'✓':'10' ?>
+
+
+
Slingshot Returned
+
= $stepReturned?'Returned — booking closed':'Mark when returned to close out & wipe card' ?>
+
+
+ ✓ Mark Returned
+
+
+
+
+
+
+
+
+
+
+
+
Admin Notes
+
+
Save Notes
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Update Card on File
+ ✕
+
+
Enter the customer's new card. The old card will be removed and replaced.
+
+
+
+ Save New Card
+ Cancel
+
+
+
+
+
+
diff --git a/view-doc.php/parker-view-doc.php b/view-doc.php/parker-view-doc.php
new file mode 100644
index 0000000..b82c898
--- /dev/null
+++ b/view-doc.php/parker-view-doc.php
@@ -0,0 +1,61 @@
+prepare("SELECT token FROM admin_tokens WHERE token=? AND expires_at > NOW()");
+ $stmt->execute([$token]);
+ return (bool)$stmt->fetch();
+}
+
+$token = preg_replace('/[^a-f0-9]/', '', $_GET['_t'] ?? '');
+if (!_verifyToken($token)) {
+ http_response_code(403);
+ header('Content-Type: text/plain');
+ exit('Unauthorized — please log in to the admin panel first.');
+}
+
+$ref = strtoupper(preg_replace('/[^A-Z0-9\-]/', '', $_GET['ref'] ?? ''));
+$type = in_array($_GET['type'] ?? '', ['license','insurance']) ? $_GET['type'] : '';
+if (!$ref || !$type) {
+ http_response_code(400);
+ header('Content-Type: text/plain');
+ exit('Missing parameters.');
+}
+
+$col = $type === 'license' ? 'license_file' : 'insurance_file';
+$stmt = db()->prepare("SELECT {$col} AS file_path FROM bookings WHERE booking_ref=?");
+$stmt->execute([$ref]);
+$row = $stmt->fetch();
+
+if (!$row || !$row['file_path']) {
+ http_response_code(404);
+ header('Content-Type: text/plain');
+ exit('Document not found.');
+}
+
+$base = realpath(__DIR__ . '/uploads');
+$path = realpath(__DIR__ . '/' . $row['file_path']);
+
+if (!$path || !$base || strpos($path, $base . DIRECTORY_SEPARATOR) !== 0) {
+ http_response_code(404);
+ header('Content-Type: text/plain');
+ exit('File not found.');
+}
+
+$finfo = new finfo(FILEINFO_MIME_TYPE);
+$mime = $finfo->file($path);
+$allowed = ['image/jpeg' => 'jpg', 'image/png' => 'png', 'application/pdf' => 'pdf'];
+if (!isset($allowed[$mime])) {
+ http_response_code(403);
+ header('Content-Type: text/plain');
+ exit('Invalid file type.');
+}
+
+$fname = $type . '-' . $ref . '.' . $allowed[$mime];
+header('Content-Type: ' . $mime);
+header('Content-Disposition: inline; filename="' . $fname . '"');
+header('Content-Length: ' . filesize($path));
+header('Cache-Control: private, max-age=3600');
+readfile($path);
+exit;