diff --git a/admin/index.php b/admin/index.php index 185b294..05cfcce 100644 --- a/admin/index.php +++ b/admin/index.php @@ -106,8 +106,8 @@ if ($isAjax) { ], 'insurance' => [ 'label' => 'Proof of Personal Auto Insurance', - 'detail' => 'You\'ll need to bring proof of valid personal auto insurance to pickup. A photo on your phone of your insurance card is fine. This is required before we can hand over the keys.', - 'cta' => '', + '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' => "
Upload Insurance Card →
", ], 'deposit' => [ 'label' => 'Balance Due at Pickup', @@ -116,8 +116,8 @@ if ($isAjax) { ], 'license' => [ 'label' => "Valid Driver's License", - 'detail' => "Please bring your valid driver's license to pickup. We're required to verify it before you take the Slingshot out. Must match the name on the booking.", - 'cta' => '', + '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' => "
Upload License Photo →
", ], ]; @@ -345,6 +345,61 @@ if ($isAjax) { 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}

+
+
+

Sign Your Rental Agreement

+ Sign Rental Agreement → +
+

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); @@ -642,8 +697,10 @@ textarea.notes-ta:focus{border-color:#f97316} $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']; @@ -792,21 +849,27 @@ textarea.notes-ta:focus{border-color:#f97316}
-
+
Proof of Insurance Received - + + + 📄 View Submitted Doc ↗ + -
+
+ + +
@@ -820,15 +883,21 @@ textarea.notes-ta:focus{border-color:#f97316}
Driver's License Verified - + + + 📄 View Submitted Doc ↗ + -
+
+ + +
@@ -1034,6 +1103,16 @@ textarea.notes-ta:focus{border-color:#f97316} Waiver Link https://parkerslingshot.epictravelexpeditions.com/waiver.php?ref=
+ +
+ Resend Confirmation Email +
+ + +
+ +
@@ -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.

+
Open Admin Panel →
+
+
"; + 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 + + + + + + +
+ Parker County Slingshot Rentals + Document Upload +
+
+ + +
+

Upload Document

+

Invalid or missing upload link. Please use the link from your email or contact us.

+
+ + +
+
+

Need help? Call or text (817) 555-0199.

+
+ + +
+
+
+

Upload Received!

+

Thanks, ! Your has been submitted for booking .

+

We'll review it and still do a quick visual check at pickup. See you on !

+
+
+ + +
+
+

Upload

+
+
+
+
+

+ + 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.

+
+
+ +
📎
+

Tap or drag your file here

+

JPG • PNG • PDF • max 10 MB

+
+
+ +
+

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' => "
Sign Agreement →
", + ], + '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' => "
Upload Insurance Card →
", + ], + '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' => "
Upload License Photo →
", + ], + ]; + + $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:

+ {$rowsHtml}
+
+

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}

+
+
+

Sign Your Rental Agreement

+ Sign Rental Agreement → +
+

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 +
+
+ + +
+
Total Bookings
+
Pending
+
Confirmed
+
Completed
+
$
Revenue
+
Waivers Signed
+
Insurance OK
+
Deposits Rcvd
+
+ + +
+
+

Bookings

+
+ + + + + +
+
+ + +
No bookings found.
+ +
+ + + + + + + + + + + + + + + $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) + )); + ?> + + + + + + + + + + + + + + + + + +
CustomerRental DatePackageAmountStatusProgressSubmitted
+ + +
+ +
+ + +
+ +
$ + + +
+
+
+
+
+
+
+
+
+
+
+ + pending + + All done ✓ + +
+
+ + +
+

Customer

+
+
+
+ +
+ +
Package
+
+
$
+
Rental Date
+
+ +
Customer Message
+
+ + +
+
Admin Notes
+ + +
+
+ + +
+

Booking Flow

+
+ + +
+
+
+ Booking Submitted + +
+
+ + +
+
+ +
+
+ Booking Confirmed + + Confirmed — status: + Cancelled + Awaiting confirmation — change status above + + +
+
+ + +
+
+ +
+
+ Rental Waiver Signed + + + Signed by + on + N/A + Not yet signed + + + + + +
+
+ + +
+
+ +
+
+ Proof of Insurance Received + + + + + 📄 View Submitted Doc ↗ + + +
+ + + + +
+ +
+
+ + +
+
+ +
+
+ Driver's License Verified + + + + + 📄 View Submitted Doc ↗ + + +
+ + + + +
+ +
+
+ + + +
+
+ +
+
+ Deposit & Balance — $ held · $ at pickup + + + •••• on file + + + + + N/A + Balance charged to card on file + Captured — $ charged + Refunded — $ + 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 + + + +
+ + + + + + + + + + + + + + + + + + +
+ +
+
+ + + +
+ Pre-Departure Checklist +
+ + +
+
+ +
+
+ DOT Helmet Provided & Fits + + + +
+ +
+
+
+ + +
+
+ +
+
+ Safety Course Completed + + + +
+ +
+
+
+ + +
+
+ +
+
+ Slingshot Operational Course Done + + + +
+ +
+
+
+ + +
+
+ +
+
+ Slingshot Returned — Close Out + + + Returned — booking closed, card data wiped + + Mark when slingshot is safely returned — closes booking & wipes card data + + + +
+ +
+ +
+
+ + +
+
+ + +
+

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.

+ +
+

Include in Reminder

+
+ + + + +
+ + +
+ +
+ Waiver Link + https://parkerslingshot.epictravelexpeditions.com/waiver.php?ref= +
+ +
+ Resend Confirmation Email +
+ + +
+ +
+ +
+ +
+
+
+ +
+ + +
+

Block Dates

+
+
+ + +
+
+ + +
+ +
+
+ +

No dates blocked.

+ + +
+ + + +
+ + +
+
+ + +
+
+

Customers

+
+ + +
+
+ + +
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+ +
+ + +
No customers yet.
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameEmailPhoneBookingsStatusAdded
+ + + + +
+
+ +
+ +
+ + + + + + + + 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;