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) 266-2022 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) )); ?>
Customer Rental Date Package Amount Status Progress Submitted


$
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.
Name Email Phone Bookings Status Added