",
],
'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' => '',
],
'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're required to verify it before you take the Slingshot out. Must match the name on the booking.",
'cta' => '',
],
];
$rowsHtml = '';
$n = 1;
foreach ($keys as $key) {
if (!isset($itemDefs[$key])) continue;
$d = $itemDefs[$key];
$rowsHtml .= "
";
$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' 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 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 === '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
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
Slingshot Operational Course Done
= $stepOps?'Operational course completed':'Walk customer through Slingshot controls before departure' ?>
= $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
Send Reminder Email
Select what the customer still needs to do, then send them a nudge email with clear instructions.
Insurance Received
= $stepInsurance?'Verified — on file':($cancelled?'N/A':'Pending — verify at pickup') ?>
= $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
= $stepLicense?'✓':($cancelled?'—':'6') ?>
Driver's License Verified
= $stepLicense?'Verified — on file':($cancelled?'N/A':'Verify at pickup') ?>