mirror of
https://github.com/myronblair/parkerslingshotrentals
synced 2026-06-30 17:50:31 -05:00
0b36064cc6
- Add PHP no-cache headers (Cache-Control: no-store) at top of file so OLS cannot cache the login page and serve it to authenticated users — this was the root cause (OLS ignores .htaccess Header directives) - Replace status filter anchor links (/admin/?status=X) with client-side JS filterBookings() — no page navigation means no cookie re-verification on each filter click, eliminating the persistent login redirect - Add data-status attribute to booking rows for JS filtering - Load all bookings at once; filter is instant and stays on same page Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1014 lines
53 KiB
PHP
1014 lines
53 KiB
PHP
<?php
|
|
// Force no-cache BEFORE any output (OLS .htaccess Header directives are unreliable)
|
|
header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
|
|
header('Pragma: no-cache');
|
|
header('Expires: Thu, 01 Jan 1970 00:00:00 GMT');
|
|
|
|
require_once dirname(__DIR__) . '/db.php';
|
|
|
|
// ── Cookie-based auth (no PHP sessions — avoids server caching/permission issues) ──
|
|
define('AUTH_COOKIE', 'parker_auth');
|
|
define('AUTH_SECRET', hash('sha256', ADMIN_PASS . ADMIN_SESSION_KEY)); // not reversible
|
|
|
|
function _authToken(): string {
|
|
$t = bin2hex(random_bytes(32));
|
|
$e = time() + 86400; // 24h
|
|
$s = hash_hmac('sha256', "$t|$e", AUTH_SECRET);
|
|
return "$t.$e.$s";
|
|
}
|
|
function _verifyAuth(): bool {
|
|
$c = $_COOKIE[AUTH_COOKIE] ?? '';
|
|
$p = explode('.', $c, 3);
|
|
if (count($p) !== 3) return false;
|
|
[$t, $e, $s] = $p;
|
|
if ((int)$e < time()) return false;
|
|
return hash_equals(hash_hmac('sha256', "$t|$e", AUTH_SECRET), $s);
|
|
}
|
|
function _setAuth(): void {
|
|
$secure = !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off';
|
|
setcookie(AUTH_COOKIE, _authToken(), [
|
|
'expires' => time() + 86400,
|
|
'path' => '/admin/',
|
|
'secure' => $secure,
|
|
'httponly' => true,
|
|
'samesite' => 'Lax',
|
|
]);
|
|
}
|
|
function _clearAuth(): void {
|
|
$secure = !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off';
|
|
setcookie(AUTH_COOKIE, '', ['expires' => time()-3600, 'path' => '/admin/', 'secure' => $secure, 'httponly' => true, 'samesite' => 'Lax']);
|
|
}
|
|
|
|
$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)) {
|
|
_setAuth();
|
|
}
|
|
header('Location: /admin/'); exit;
|
|
}
|
|
if (($_GET['action'] ?? '') === 'logout') {
|
|
_clearAuth(); header('Location: /admin/'); exit;
|
|
}
|
|
$authed = _verifyAuth();
|
|
|
|
// ── AJAX handlers ─────────────────────────────────────────────────────────────
|
|
if ($isAjax && !$authed) {
|
|
http_response_code(401);
|
|
header('Content-Type: application/json');
|
|
echo json_encode(['error'=>'Session expired. Please reload and 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'];
|
|
if ($id && in_array($field, $allowed_fields)) {
|
|
$stmt = db()->prepare("SELECT `{$field}` FROM bookings WHERE id=?");
|
|
$stmt->execute([$id]);
|
|
$current = (int)$stmt->fetchColumn();
|
|
$new = $current ? 0 : 1;
|
|
db()->prepare("UPDATE bookings SET `{$field}`=? WHERE id=?")->execute([$new, $id]);
|
|
echo json_encode(['ok'=>true,'value'=>$new]);
|
|
} else { echo json_encode(['error'=>'Invalid']); }
|
|
exit;
|
|
}
|
|
|
|
if ($action === 'send_reminder') {
|
|
$id = (int)($_POST['id'] ?? 0);
|
|
$keys = array_filter(explode(',', $_POST['items'] ?? ''));
|
|
$stmt = db()->prepare("SELECT * FROM bookings WHERE id=?");
|
|
$stmt->execute([$id]);
|
|
$b = $stmt->fetch();
|
|
if (!$b) { echo json_encode(['error'=>'Not found']); exit; }
|
|
|
|
$pkg = PACKAGES[$b['package']] ?? ['label' => $b['package']];
|
|
$dateLabel = date('F j, Y', strtotime($b['rental_date']));
|
|
$ref = $b['booking_ref'];
|
|
|
|
$itemDefs = [
|
|
'waiver' => [
|
|
'label' => 'Sign Your Rental Agreement',
|
|
'detail' => 'Your digital rental agreement still needs to be signed before your pickup. It only takes a minute and can be done on any device — no printer required.',
|
|
'cta' => "<div style='margin-top:10px'><a href='https://parkerslingshotrentals.com/waiver.php?ref={$ref}' style='display:inline-block;background:#f97316;color:#fff;text-decoration:none;padding:9px 20px;border-radius:6px;font-weight:700;font-size:13px'>Sign Agreement →</a></div>",
|
|
],
|
|
'insurance' => [
|
|
'label' => 'Proof of Personal Auto Insurance',
|
|
'detail' => 'You\'ll need to 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 .= "
|
|
<tr>
|
|
<td style='padding:14px 0;border-bottom:1px solid #f3f4f6;vertical-align:top;width:30px'>
|
|
<span style='display:inline-flex;align-items:center;justify-content:center;width:24px;height:24px;border-radius:50%;background:#fff7ed;border:2px solid #f97316;font-size:11px;font-weight:800;color:#f97316;flex-shrink:0'>{$n}</span>
|
|
</td>
|
|
<td style='padding:14px 0 14px 14px;border-bottom:1px solid #f3f4f6'>
|
|
<strong style='color:#111;font-size:14px'>" . htmlspecialchars($d['label']) . "</strong>
|
|
<p style='margin:5px 0 0;font-size:13px;color:#6b7280;line-height:1.5'>" . htmlspecialchars($d['detail']) . "</p>
|
|
{$d['cta']}
|
|
</td>
|
|
</tr>";
|
|
$n++;
|
|
}
|
|
|
|
if (!$rowsHtml) { echo json_encode(['error'=>'No items selected']); exit; }
|
|
|
|
$html = "
|
|
<div style='max-width:600px;margin:0 auto;font-family:Arial,sans-serif'>
|
|
<div style='background:#0d0d0d;padding:24px;text-align:center'>
|
|
<h1 style='color:#f97316;margin:0;font-size:20px'>Parker County Slingshot Rentals</h1>
|
|
</div>
|
|
<div style='padding:32px;background:#fff'>
|
|
<h2 style='margin-top:0;color:#111'>Almost Ready — A Few Things Before Pickup</h2>
|
|
<p style='color:#374151;margin-bottom:8px'>Hey " . htmlspecialchars($b['name']) . ", your <strong>" . htmlspecialchars($pkg['label']) . "</strong> rental on <strong>{$dateLabel}</strong> is coming up! (Ref: <strong>{$ref}</strong>)</p>
|
|
<p style='color:#374151;margin-bottom:20px'>To make sure pickup goes smoothly, here's what still needs to be taken care of:</p>
|
|
<table style='width:100%;border-collapse:collapse'>{$rowsHtml}</table>
|
|
<div style='margin-top:24px;padding:16px;background:#f9fafb;border-radius:8px'>
|
|
<p style='margin:0;font-size:13px;color:#6b7280'>Questions? Call or text <strong style='color:#111'>(817) 555-0199</strong> or reply to this email — we're happy to help.</p>
|
|
</div>
|
|
<p style='color:#374151;margin-top:24px'>Ride on,<br><strong>The Parker County Slingshot Team</strong></p>
|
|
</div>
|
|
<div style='background:#f3f4f6;padding:16px;text-align:center'>
|
|
<p style='margin:0;font-size:12px;color:#9ca3af'>© " . date('Y') . " Parker County Slingshot Rentals — Weatherford, TX</p>
|
|
</div>
|
|
</div>";
|
|
|
|
$sent = sendEmail($b['email'], $b['name'], "Action Needed Before Your Rental — {$ref}", $html);
|
|
echo json_encode(['ok'=>true]);
|
|
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 === '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]);
|
|
echo json_encode(['ok'=>true]);
|
|
} 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;
|
|
}
|
|
exit;
|
|
}
|
|
|
|
// ── Login page ─────────────────────────────────────────────────────────────────
|
|
if (!$authed) { ?>
|
|
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
<title>Admin Login — Parker County Slingshot Rentals</title>
|
|
<style>
|
|
*{box-sizing:border-box;margin:0;padding:0}
|
|
body{font-family:system-ui,sans-serif;background:#0d0d0d;display:flex;align-items:center;justify-content:center;min-height:100vh}
|
|
.box{background:#1a1a1a;border:1px solid rgba(255,255,255,.1);border-radius:12px;padding:2.5rem;width:100%;max-width:360px}
|
|
h1{color:#f97316;font-size:1.4rem;margin-bottom:.25rem}
|
|
p{color:rgba(255,255,255,.4);font-size:.85rem;margin-bottom:2rem}
|
|
label{display:block;color:rgba(255,255,255,.6);font-size:.85rem;margin-bottom:.4rem}
|
|
input{width:100%;background:rgba(255,255,255,.06);border:1px solid rgba(255,255,255,.12);border-radius:8px;padding:.75rem 1rem;color:#fff;font-size:.95rem;margin-bottom:1.25rem;outline:none}
|
|
input:focus{border-color:#f97316}
|
|
button{width:100%;background:#f97316;color:#fff;border:none;border-radius:8px;padding:.85rem;font-size:1rem;font-weight:700;cursor:pointer}
|
|
button:hover{background:#ea580c}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="box">
|
|
<h1>Parker Admin</h1>
|
|
<p>Slingshot Rentals Management</p>
|
|
<form method="POST">
|
|
<input type="hidden" name="action" value="login">
|
|
<label>Username</label>
|
|
<input type="text" name="username" required autofocus>
|
|
<label>Password</label>
|
|
<input type="password" name="password" required>
|
|
<button type="submit">Sign In</button>
|
|
</form>
|
|
</div>
|
|
</body>
|
|
</html>
|
|
<?php exit; }
|
|
|
|
// ── Dashboard data ─────────────────────────────────────────────────────────────
|
|
$bookings = db()->query("SELECT * FROM bookings ORDER BY rental_date ASC, created_at DESC")->fetchAll();
|
|
$blocked = db()->query("SELECT * FROM blocked_dates ORDER BY block_date ASC")->fetchAll();
|
|
|
|
$stats = db()->query("
|
|
SELECT
|
|
COUNT(*) AS total,
|
|
SUM(status='pending') AS pending,
|
|
SUM(status='confirmed') AS confirmed,
|
|
SUM(status='completed') AS completed,
|
|
SUM(status='cancelled') AS cancelled,
|
|
SUM(CASE WHEN status IN ('confirmed','completed') THEN amount ELSE 0 END) AS revenue,
|
|
SUM(waiver_signed) AS waivers_signed,
|
|
SUM(insurance_verified) AS insurance_done,
|
|
SUM(deposit_received) AS deposits_done
|
|
FROM bookings
|
|
")->fetch();
|
|
?>
|
|
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
<title>Admin — Parker County Slingshot Rentals</title>
|
|
<style>
|
|
*{box-sizing:border-box;margin:0;padding:0}
|
|
body{font-family:system-ui,sans-serif;background:#f3f4f6;color:#111;min-height:100vh}
|
|
header{background:#0d0d0d;color:#fff;padding:1rem 2rem;display:flex;align-items:center;justify-content:space-between;gap:1rem}
|
|
header h1{color:#f97316;font-size:1.15rem;font-weight:800;white-space:nowrap}
|
|
header a{color:rgba(255,255,255,.5);font-size:.85rem;text-decoration:none;white-space:nowrap}
|
|
header a:hover{color:#fff}
|
|
.main{max-width:1300px;margin:0 auto;padding:2rem 1.5rem}
|
|
|
|
/* Stats */
|
|
.stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(130px,1fr));gap:1rem;margin-bottom:2rem}
|
|
.stat{background:#fff;border-radius:10px;padding:1.1rem;border:1px solid #e5e7eb;text-align:center}
|
|
.stat-val{font-size:1.75rem;font-weight:800;line-height:1}
|
|
.stat-lbl{font-size:.72rem;color:#6b7280;margin-top:.3rem;text-transform:uppercase;letter-spacing:.5px}
|
|
|
|
/* Card */
|
|
.card{background:#fff;border-radius:10px;border:1px solid #e5e7eb;margin-bottom:2rem;overflow:hidden}
|
|
.card-header{padding:1rem 1.5rem;border-bottom:1px solid #f3f4f6;display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:.75rem}
|
|
.card-header h2{font-size:1rem;font-weight:700}
|
|
.filters .filter-btn{padding:.35rem .75rem;border-radius:6px;font-size:.8rem;color:#6b7280;border:1px solid #e5e7eb;margin-left:.35rem;background:#fff;cursor:pointer;font-family:inherit}
|
|
.filters .filter-btn.active,.filters .filter-btn:hover{background:#f97316;color:#fff;border-color:#f97316}
|
|
|
|
/* Table */
|
|
table{width:100%;border-collapse:collapse;font-size:.875rem}
|
|
th{background:#f9fafb;padding:.6rem 1rem;text-align:left;font-weight:600;color:#6b7280;font-size:.72rem;text-transform:uppercase;letter-spacing:.5px;border-bottom:1px solid #e5e7eb;white-space:nowrap}
|
|
td{padding:.7rem 1rem;border-bottom:1px solid #f3f4f6;vertical-align:middle}
|
|
.booking-row td{cursor:pointer}
|
|
.booking-row:hover td{background:#fafafa}
|
|
.detail-row td{padding:0;background:#f9fafb}
|
|
.detail-row.open td{border-bottom:3px solid #f97316}
|
|
|
|
/* Expand button */
|
|
.expand-btn{background:none;border:none;cursor:pointer;color:#9ca3af;font-size:1rem;padding:2px 6px;border-radius:4px;transition:transform .2s,color .2s;line-height:1}
|
|
.expand-btn.open{transform:rotate(90deg);color:#f97316}
|
|
|
|
/* Progress dots */
|
|
.dots{display:flex;gap:4px;align-items:center}
|
|
.dot{width:11px;height:11px;border-radius:50%;flex-shrink:0;transition:background .2s}
|
|
.dot-done{background:#22c55e}
|
|
.dot-skip{background:#d1d5db}
|
|
.dot-pending{background:#fbbf24}
|
|
|
|
/* Status badge / select */
|
|
.badge{display:inline-block;padding:.2rem .6rem;border-radius:999px;font-size:.72rem;font-weight:700;text-transform:uppercase;letter-spacing:.4px}
|
|
select.status-sel{font-size:.8rem;border:1px solid #e5e7eb;border-radius:6px;padding:.25rem .5rem;cursor:pointer;background:#fff}
|
|
|
|
/* ── Detail panel ───────────────────────────────────────── */
|
|
.detail-panel{display:grid;grid-template-columns:240px 1fr 1fr;gap:0;border-top:1px solid #e5e7eb}
|
|
@media(max-width:900px){.detail-panel{grid-template-columns:1fr}}
|
|
.dp-col{padding:1.5rem;border-right:1px solid #f3f4f6}
|
|
.dp-col:last-child{border-right:none}
|
|
.dp-col h3{font-size:.72rem;font-weight:700;text-transform:uppercase;letter-spacing:1px;color:#9ca3af;margin-bottom:1rem}
|
|
|
|
/* Contact info */
|
|
.ci-name{font-size:1rem;font-weight:700;margin-bottom:.2rem}
|
|
.ci-row{display:flex;gap:.5rem;align-items:baseline;font-size:.85rem;color:#6b7280;margin-bottom:.35rem}
|
|
.ci-row a{color:#f97316;text-decoration:none}
|
|
.ci-row a:hover{text-decoration:underline}
|
|
.ci-ref{display:inline-block;font-size:.75rem;font-weight:700;color:#f97316;background:#fff7ed;padding:.15rem .5rem;border-radius:4px;margin-bottom:.75rem}
|
|
.ci-field{font-size:.78rem;color:#9ca3af;text-transform:uppercase;letter-spacing:.5px;margin-bottom:.15rem;margin-top:.6rem}
|
|
|
|
/* Flow checklist */
|
|
.flow-list{display:flex;flex-direction:column;gap:.15rem}
|
|
.flow-step{display:flex;align-items:flex-start;gap:.75rem;padding:.65rem .5rem;border-radius:8px;transition:background .15s}
|
|
.flow-step:hover{background:rgba(0,0,0,.02)}
|
|
.flow-icon{width:26px;height:26px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:.75rem;font-weight:700;flex-shrink:0;margin-top:1px}
|
|
.flow-icon.done{background:#dcfce7;color:#16a34a}
|
|
.flow-icon.pending{background:#fef3c7;color:#d97706;font-size:.65rem}
|
|
.flow-icon.skip{background:#f3f4f6;color:#9ca3af}
|
|
.flow-body{flex:1;min-width:0}
|
|
.flow-label{font-size:.88rem;font-weight:600;color:#111;display:block}
|
|
.flow-label.muted{color:#9ca3af}
|
|
.flow-meta{font-size:.75rem;color:#9ca3af;margin-top:.1rem;display:block}
|
|
.flow-action{margin-top:.35rem}
|
|
.flow-toggle{background:none;border:1px solid #d1d5db;border-radius:5px;font-size:.73rem;padding:.2rem .6rem;cursor:pointer;color:#374151;transition:all .15s}
|
|
.flow-toggle:hover{border-color:#f97316;color:#f97316}
|
|
.flow-toggle.active{background:#dcfce7;border-color:#bbf7d0;color:#15803d}
|
|
.flow-link{font-size:.75rem;color:#f97316;text-decoration:none}
|
|
.flow-link:hover{text-decoration:underline}
|
|
|
|
/* Notes */
|
|
textarea.notes-ta{width:100%;font-size:.82rem;border:1px solid #e5e7eb;border-radius:6px;padding:.5rem .65rem;resize:vertical;min-height:72px;font-family:inherit;outline:none;transition:border-color .2s}
|
|
textarea.notes-ta:focus{border-color:#f97316}
|
|
.save-btn{margin-top:.5rem;font-size:.78rem;background:#f97316;color:#fff;border:none;border-radius:5px;padding:.3rem .75rem;cursor:pointer;transition:background .15s}
|
|
.save-btn:hover{background:#ea580c}
|
|
|
|
/* Reminder section */
|
|
.reminder-box{background:#fff7ed;border:1px solid #fed7aa;border-radius:8px;padding:1rem;margin-top:1.25rem}
|
|
.reminder-box h4{font-size:.78rem;font-weight:700;text-transform:uppercase;letter-spacing:.5px;color:#92400e;margin-bottom:.65rem}
|
|
.reminder-items{display:flex;flex-direction:column;gap:.4rem;margin-bottom:.85rem}
|
|
.reminder-items label{display:flex;align-items:flex-start;gap:.5rem;font-size:.83rem;color:#374151;cursor:pointer}
|
|
.reminder-items input[type=checkbox]{margin-top:2px;accent-color:#f97316;width:14px;height:14px}
|
|
.send-btn{width:100%;background:#f97316;color:#fff;border:none;border-radius:6px;padding:.55rem;font-size:.83rem;font-weight:700;cursor:pointer;transition:background .15s}
|
|
.send-btn:hover{background:#ea580c}
|
|
.send-btn:disabled{background:#d1d5db;cursor:not-allowed}
|
|
|
|
/* Block dates */
|
|
.block-form{display:flex;gap:.75rem;flex-wrap:wrap;align-items:flex-end;padding:1rem 1.5rem}
|
|
.block-form input{border:1px solid #e5e7eb;border-radius:6px;padding:.45rem .75rem;font-size:.875rem}
|
|
.block-form button{background:#dc2626;color:#fff;border:none;border-radius:6px;padding:.45rem 1rem;font-size:.875rem;font-weight:600;cursor:pointer}
|
|
.block-list{padding:0 1.5rem 1rem}
|
|
.block-item{display:flex;align-items:center;gap:.75rem;padding:.5rem 0;border-bottom:1px solid #f3f4f6;font-size:.875rem}
|
|
.block-item:last-child{border-bottom:none}
|
|
.del-btn{background:none;border:none;color:#dc2626;cursor:pointer;font-size:1rem;padding:0;line-height:1}
|
|
|
|
@media(max-width:700px){.main{padding:1rem}.stat-val{font-size:1.5rem}}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<header>
|
|
<h1>Parker County Slingshot — Admin</h1>
|
|
<a href="?action=logout">Sign Out</a>
|
|
</header>
|
|
<div class="main">
|
|
|
|
<!-- Stats -->
|
|
<div class="stats">
|
|
<div class="stat"><div class="stat-val"><?= (int)$stats['total'] ?></div><div class="stat-lbl">Total Bookings</div></div>
|
|
<div class="stat"><div class="stat-val" style="color:#d97706"><?= (int)$stats['pending'] ?></div><div class="stat-lbl">Pending</div></div>
|
|
<div class="stat"><div class="stat-val" style="color:#16a34a"><?= (int)$stats['confirmed'] ?></div><div class="stat-lbl">Confirmed</div></div>
|
|
<div class="stat"><div class="stat-val" style="color:#2563eb"><?= (int)$stats['completed'] ?></div><div class="stat-lbl">Completed</div></div>
|
|
<div class="stat"><div class="stat-val" style="color:#f97316">$<?= number_format((float)$stats['revenue'],0) ?></div><div class="stat-lbl">Revenue</div></div>
|
|
<div class="stat"><div class="stat-val" style="color:#16a34a"><?= (int)$stats['waivers_signed'] ?></div><div class="stat-lbl">Waivers Signed</div></div>
|
|
<div class="stat"><div class="stat-val" style="color:#16a34a"><?= (int)$stats['insurance_done'] ?></div><div class="stat-lbl">Insurance OK</div></div>
|
|
<div class="stat"><div class="stat-val" style="color:#16a34a"><?= (int)$stats['deposits_done'] ?></div><div class="stat-lbl">Deposits Rcvd</div></div>
|
|
</div>
|
|
|
|
<!-- Bookings -->
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h2>Bookings</h2>
|
|
<div class="filters">
|
|
<button class="filter-btn active" onclick="filterBookings('all',this)">All</button>
|
|
<button class="filter-btn" onclick="filterBookings('pending',this)">Pending</button>
|
|
<button class="filter-btn" onclick="filterBookings('confirmed',this)">Confirmed</button>
|
|
<button class="filter-btn" onclick="filterBookings('completed',this)">Completed</button>
|
|
<button class="filter-btn" onclick="filterBookings('cancelled',this)">Cancelled</button>
|
|
</div>
|
|
</div>
|
|
|
|
<?php if (empty($bookings)): ?>
|
|
<div style="padding:3rem;text-align:center;color:#9ca3af">No bookings found.</div>
|
|
<?php else: ?>
|
|
<div style="overflow-x:auto">
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th style="width:36px"></th>
|
|
<th>Customer</th>
|
|
<th>Rental Date</th>
|
|
<th>Package</th>
|
|
<th>Amount</th>
|
|
<th>Status</th>
|
|
<th>Progress</th>
|
|
<th>Submitted</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php foreach ($bookings as $b):
|
|
$bid = $b['id'];
|
|
$pkg = PACKAGES[$b['package']] ?? ['label'=>$b['package']];
|
|
|
|
// Determine each step's state
|
|
$stepConfirmed = in_array($b['status'], ['confirmed','completed']);
|
|
$stepWaiver = (bool)$b['waiver_signed'];
|
|
$stepInsurance = (bool)$b['insurance_verified'];
|
|
$stepDeposit = (bool)$b['deposit_received'];
|
|
$stepLicense = (bool)$b['license_verified'];
|
|
|
|
// 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;
|
|
$pendingCount = ($cancelled ? 0 : (
|
|
(!$stepConfirmed?1:0)+(!$stepWaiver?1:0)+(!$stepInsurance?1:0)+(!$stepDeposit?1:0)+(!$stepLicense?1:0)
|
|
));
|
|
?>
|
|
<tr class="booking-row" data-status="<?= htmlspecialchars($b['status']) ?>" onclick="toggleDetail(<?= $bid ?>)">
|
|
<td onclick="event.stopPropagation()">
|
|
<button class="expand-btn" id="expand-<?= $bid ?>" onclick="toggleDetail(<?= $bid ?>)">►</button>
|
|
</td>
|
|
<td>
|
|
<strong><?= htmlspecialchars($b['name']) ?></strong><br>
|
|
<span style="font-size:.78rem;color:#9ca3af"><?= htmlspecialchars($b['email']) ?></span>
|
|
</td>
|
|
<td>
|
|
<strong><?= date('M j, Y', strtotime($b['rental_date'])) ?></strong>
|
|
<?php if ($b['end_date'] !== $b['rental_date']): ?>
|
|
<br><span style="font-size:.75rem;color:#9ca3af">→ <?= date('M j', strtotime($b['end_date'])) ?></span>
|
|
<?php endif; ?>
|
|
</td>
|
|
<td style="font-size:.83rem"><?= htmlspecialchars($pkg['label']) ?></td>
|
|
<td style="font-weight:600">$<?= number_format($b['amount'],2) ?></td>
|
|
<td onclick="event.stopPropagation()">
|
|
<select class="status-sel" data-id="<?= $bid ?>" onchange="updateStatus(this)">
|
|
<?php foreach (['pending','confirmed','completed','cancelled'] as $s): ?>
|
|
<option value="<?= $s ?>" <?= $b['status']===$s?'selected':'' ?>><?= ucfirst($s) ?></option>
|
|
<?php endforeach; ?>
|
|
</select>
|
|
</td>
|
|
<td>
|
|
<div class="dots" title="Confirmed / Waiver / Insurance / Deposit / License">
|
|
<div class="dot <?= $dotClass($stepConfirmed) ?>" title="Confirmed"></div>
|
|
<div class="dot <?= $dotClass($stepWaiver) ?>" title="Waiver Signed"></div>
|
|
<div class="dot <?= $dotClass($stepInsurance) ?>" title="Insurance" id="dot-<?= $bid ?>-insurance_verified"></div>
|
|
<div class="dot <?= $dotClass($stepDeposit) ?>" title="Deposit" id="dot-<?= $bid ?>-deposit_received"></div>
|
|
<div class="dot <?= $dotClass($stepLicense) ?>" title="License" id="dot-<?= $bid ?>-license_verified"></div>
|
|
</div>
|
|
<?php if (!$cancelled && $pendingCount): ?>
|
|
<span style="font-size:.68rem;color:#d97706;display:block;margin-top:3px"><?= $pendingCount ?> pending</span>
|
|
<?php elseif ($allDone): ?>
|
|
<span style="font-size:.68rem;color:#16a34a;display:block;margin-top:3px">All done ✓</span>
|
|
<?php endif; ?>
|
|
</td>
|
|
<td style="font-size:.78rem;color:#9ca3af;white-space:nowrap"><?= date('M j g:ia', strtotime($b['created_at'])) ?></td>
|
|
</tr>
|
|
|
|
<!-- ── Detail Panel ───────────────────────────────────────────── -->
|
|
<tr class="detail-row" id="detail-<?= $bid ?>" data-status="<?= htmlspecialchars($b['status']) ?>">
|
|
<td colspan="8">
|
|
<div class="detail-panel">
|
|
|
|
<!-- Column 1: Contact info -->
|
|
<div class="dp-col">
|
|
<h3>Customer</h3>
|
|
<div class="ci-ref"><?= htmlspecialchars($b['booking_ref']) ?></div>
|
|
<div class="ci-name"><?= htmlspecialchars($b['name']) ?></div>
|
|
<div class="ci-row"><a href="mailto:<?= htmlspecialchars($b['email']) ?>"><?= htmlspecialchars($b['email']) ?></a></div>
|
|
<?php if ($b['phone']): ?>
|
|
<div class="ci-row"><a href="tel:<?= htmlspecialchars($b['phone']) ?>"><?= htmlspecialchars($b['phone']) ?></a></div>
|
|
<?php endif; ?>
|
|
<div class="ci-field">Package</div>
|
|
<div style="font-size:.85rem;font-weight:600"><?= htmlspecialchars($pkg['label']) ?></div>
|
|
<div style="font-size:.82rem;color:#6b7280">$<?= number_format($b['amount'],2) ?></div>
|
|
<div class="ci-field">Rental Date</div>
|
|
<div style="font-size:.85rem;font-weight:600"><?= date('F j, Y', strtotime($b['rental_date'])) ?></div>
|
|
<?php if ($b['notes']): ?>
|
|
<div class="ci-field">Customer Message</div>
|
|
<div style="font-size:.82rem;color:#6b7280;font-style:italic"><?= nl2br(htmlspecialchars($b['notes'])) ?></div>
|
|
<?php endif; ?>
|
|
|
|
<div style="margin-top:1.25rem">
|
|
<div class="ci-field">Admin Notes</div>
|
|
<textarea class="notes-ta" id="notes-<?= $bid ?>"><?= htmlspecialchars($b['admin_notes'] ?? '') ?></textarea>
|
|
<button class="save-btn" onclick="saveNotes(<?= $bid ?>)">Save Notes</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Column 2: Booking flow checklist -->
|
|
<div class="dp-col">
|
|
<h3>Booking Flow</h3>
|
|
<div class="flow-list">
|
|
|
|
<!-- Step 1: Submitted -->
|
|
<div class="flow-step">
|
|
<div class="flow-icon done">✓</div>
|
|
<div class="flow-body">
|
|
<span class="flow-label">Booking Submitted</span>
|
|
<span class="flow-meta"><?= date('M j, Y g:ia', strtotime($b['created_at'])) ?></span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Step 2: Confirmed -->
|
|
<div class="flow-step">
|
|
<div class="flow-icon <?= $stepConfirmed?'done':($cancelled?'skip':'pending') ?>">
|
|
<?= $stepConfirmed?'✓':($cancelled?'—':'2') ?>
|
|
</div>
|
|
<div class="flow-body">
|
|
<span class="flow-label <?= (!$stepConfirmed&&!$cancelled)?'':''; ?>">Booking Confirmed</span>
|
|
<span class="flow-meta">
|
|
<?php if ($stepConfirmed): ?>Confirmed — status: <?= ucfirst($b['status']) ?>
|
|
<?php elseif ($cancelled): ?>Cancelled
|
|
<?php else: ?>Awaiting confirmation — change status above
|
|
<?php endif; ?>
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Step 3: Waiver -->
|
|
<div class="flow-step">
|
|
<div class="flow-icon <?= $stepWaiver?'done':($cancelled?'skip':'pending') ?>">
|
|
<?= $stepWaiver?'✓':($cancelled?'—':'3') ?>
|
|
</div>
|
|
<div class="flow-body">
|
|
<span class="flow-label">Rental Waiver Signed</span>
|
|
<span class="flow-meta">
|
|
<?php if ($stepWaiver): ?>
|
|
Signed by <?= htmlspecialchars($b['waiver_name'] ?? $b['name']) ?>
|
|
<?php if ($b['waiver_signed_at']): ?> on <?= date('M j g:ia', strtotime($b['waiver_signed_at'])) ?><?php endif; ?>
|
|
<?php elseif ($cancelled): ?>N/A
|
|
<?php else: ?>Not yet signed
|
|
<?php endif; ?>
|
|
</span>
|
|
<?php if (!$stepWaiver && !$cancelled): ?>
|
|
<div class="flow-action">
|
|
<a class="flow-link" href="https://parkerslingshotrentals.com/waiver.php?ref=<?= urlencode($b['booking_ref']) ?>" target="_blank">Open Waiver Link ↗</a>
|
|
</div>
|
|
<?php endif; ?>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Step 4: Insurance -->
|
|
<div class="flow-step">
|
|
<div class="flow-icon <?= $stepInsurance?'done':($cancelled?'skip':'pending') ?>" id="icon-<?= $bid ?>-insurance_verified">
|
|
<?= $stepInsurance?'✓':($cancelled?'—':'4') ?>
|
|
</div>
|
|
<div class="flow-body">
|
|
<span class="flow-label">Proof of Insurance Received</span>
|
|
<span class="flow-meta" id="meta-<?= $bid ?>-insurance_verified">
|
|
<?= $stepInsurance?'Verified — on file':($cancelled?'N/A':'Pending — verify at pickup') ?>
|
|
</span>
|
|
<?php if (!$cancelled): ?>
|
|
<div class="flow-action">
|
|
<button class="flow-toggle <?= $stepInsurance?'active':'' ?>"
|
|
id="btn-<?= $bid ?>-insurance_verified"
|
|
onclick="toggleReq(<?= $bid ?>,'insurance_verified',this)">
|
|
<?= $stepInsurance?'✓ Marked Received':'Mark Received' ?>
|
|
</button>
|
|
</div>
|
|
<?php endif; ?>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Step 5: Deposit (Square-aware) -->
|
|
<?php
|
|
$sqStatus = $b['square_payment_status'] ?? '';
|
|
$sqId = $b['square_payment_id'] ?? '';
|
|
$depositIcon = 'pending';
|
|
$depositLabel = '5';
|
|
if ($cancelled) { $depositIcon='skip'; $depositLabel='—'; }
|
|
elseif ($stepDeposit ||
|
|
$sqStatus==='COMPLETED') { $depositIcon='done'; $depositLabel='✓'; }
|
|
?>
|
|
<div class="flow-step">
|
|
<div class="flow-icon <?= $depositIcon ?>" id="icon-<?= $bid ?>-deposit_received">
|
|
<?= $depositLabel ?>
|
|
</div>
|
|
<div class="flow-body">
|
|
<span class="flow-label">Deposit & Balance — $<?= number_format(DEPOSIT_AMOUNT,2) ?> held · $<?= number_format($b['amount']-DEPOSIT_AMOUNT,2) ?> at pickup</span>
|
|
<span class="flow-meta" id="meta-<?= $bid ?>-deposit_received">
|
|
<?php if ($cancelled): ?>N/A
|
|
<?php elseif ($sqStatus === 'COMPLETED'): ?>Captured — $<?= number_format((float)($b['deposit_paid']??DEPOSIT_AMOUNT),2) ?> charged
|
|
<?php elseif ($sqStatus === 'REFUNDED'): ?>Refunded — $<?= number_format((float)($b['deposit_paid']??DEPOSIT_AMOUNT),2) ?>
|
|
<?php elseif ($sqStatus === 'CANCELED'): ?>Hold voided — no charge
|
|
<?php elseif ($sqStatus === 'APPROVED' || $sqStatus === 'PENDING'): ?>Hold active — card authorized, not yet charged
|
|
<?php elseif ($stepDeposit): ?>Deposit marked received (manual)
|
|
<?php else: ?>Pending — no card on file yet
|
|
<?php endif; ?>
|
|
</span>
|
|
<?php if (!$cancelled): ?>
|
|
<div class="flow-action" id="deposit-actions-<?= $bid ?>">
|
|
<?php if ($sqStatus === 'APPROVED' || $sqStatus === 'PENDING'): ?>
|
|
<button class="flow-toggle" onclick="squareAction(<?= $bid ?>,'square_capture',this)" style="margin-right:4px">Capture $<?= number_format(DEPOSIT_AMOUNT,0) ?></button>
|
|
<button class="flow-toggle" onclick="squareAction(<?= $bid ?>,'square_void',this)" style="border-color:#dc2626;color:#dc2626">Void Hold</button>
|
|
<?php elseif ($sqStatus === 'COMPLETED'): ?>
|
|
<button class="flow-toggle" onclick="squareAction(<?= $bid ?>,'square_refund',this)" style="border-color:#2563eb;color:#2563eb">Refund $<?= number_format((float)($b['deposit_paid']??DEPOSIT_AMOUNT),2) ?></button>
|
|
<?php elseif (!$sqId): ?>
|
|
<button class="flow-toggle <?= $stepDeposit?'active':'' ?>"
|
|
id="btn-<?= $bid ?>-deposit_received"
|
|
onclick="toggleReq(<?= $bid ?>,'deposit_received',this)">
|
|
<?= $stepDeposit?'✓ Deposit Received':'Mark Deposit Received' ?>
|
|
</button>
|
|
<?php endif; ?>
|
|
</div>
|
|
<?php endif; ?>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Step 6: License -->
|
|
<div class="flow-step">
|
|
<div class="flow-icon <?= $stepLicense?'done':($cancelled?'skip':'pending') ?>" id="icon-<?= $bid ?>-license_verified">
|
|
<?= $stepLicense?'✓':($cancelled?'—':'6') ?>
|
|
</div>
|
|
<div class="flow-body">
|
|
<span class="flow-label">Driver's License Verified</span>
|
|
<span class="flow-meta" id="meta-<?= $bid ?>-license_verified">
|
|
<?= $stepLicense?'License verified':($cancelled?'N/A':'Verify at pickup') ?>
|
|
</span>
|
|
<?php if (!$cancelled): ?>
|
|
<div class="flow-action">
|
|
<button class="flow-toggle <?= $stepLicense?'active':'' ?>"
|
|
id="btn-<?= $bid ?>-license_verified"
|
|
onclick="toggleReq(<?= $bid ?>,'license_verified',this)">
|
|
<?= $stepLicense?'✓ License Verified':'Mark License Verified' ?>
|
|
</button>
|
|
</div>
|
|
<?php endif; ?>
|
|
</div>
|
|
</div>
|
|
|
|
</div><!-- /.flow-list -->
|
|
</div><!-- /.dp-col flow -->
|
|
|
|
<!-- Column 3: Reminder -->
|
|
<div class="dp-col">
|
|
<h3>Send Reminder Email</h3>
|
|
<p style="font-size:.82rem;color:#6b7280;margin-bottom:1rem">Select what the customer still needs to do, then send them a nudge email with clear instructions.</p>
|
|
|
|
<?php if ($cancelled): ?>
|
|
<p style="font-size:.82rem;color:#9ca3af">Not applicable for cancelled bookings.</p>
|
|
<?php else: ?>
|
|
<div class="reminder-box">
|
|
<h4>Include in Reminder</h4>
|
|
<div class="reminder-items" id="reminder-<?= $bid ?>">
|
|
<label>
|
|
<input type="checkbox" value="waiver" <?= !$stepWaiver?'checked':'' ?>>
|
|
Sign rental agreement
|
|
</label>
|
|
<label>
|
|
<input type="checkbox" value="insurance" <?= !$stepInsurance?'checked':'' ?>>
|
|
Bring proof of insurance
|
|
</label>
|
|
<label>
|
|
<input type="checkbox" value="deposit" <?= !$stepDeposit?'checked':'' ?>>
|
|
Security deposit info
|
|
</label>
|
|
<label>
|
|
<input type="checkbox" value="license" <?= !$stepLicense?'checked':'' ?>>
|
|
Bring driver's license
|
|
</label>
|
|
</div>
|
|
<button class="send-btn" id="remind-btn-<?= $bid ?>" onclick="sendReminder(<?= $bid ?>)">
|
|
Send Reminder Email
|
|
</button>
|
|
<div id="remind-status-<?= $bid ?>" style="margin-top:.6rem;font-size:.78rem;display:none"></div>
|
|
</div>
|
|
|
|
<div style="margin-top:1.25rem;padding:1rem;background:#f9fafb;border-radius:8px;font-size:.8rem;color:#6b7280">
|
|
<strong style="color:#111;display:block;margin-bottom:.35rem">Waiver Link</strong>
|
|
<code style="word-break:break-all;font-size:.72rem">https://parkerslingshotrentals.com/waiver.php?ref=<?= htmlspecialchars($b['booking_ref']) ?></code>
|
|
</div>
|
|
<?php endif; ?>
|
|
</div>
|
|
|
|
</div><!-- /.detail-panel -->
|
|
</td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<?php endif; ?>
|
|
</div>
|
|
|
|
<!-- Block Dates -->
|
|
<div class="card">
|
|
<div class="card-header"><h2>Block Dates</h2></div>
|
|
<form class="block-form" onsubmit="blockDate(event)">
|
|
<div>
|
|
<label style="font-size:.8rem;color:#6b7280;display:block;margin-bottom:.25rem">Date</label>
|
|
<input type="date" id="blockDate" required min="<?= date('Y-m-d') ?>">
|
|
</div>
|
|
<div style="flex:1;min-width:180px">
|
|
<label style="font-size:.8rem;color:#6b7280;display:block;margin-bottom:.25rem">Reason (optional)</label>
|
|
<input type="text" id="blockReason" placeholder="e.g. Maintenance, personal use">
|
|
</div>
|
|
<button type="submit">Block Date</button>
|
|
</form>
|
|
<div class="block-list">
|
|
<?php if (empty($blocked)): ?>
|
|
<p style="color:#9ca3af;font-size:.875rem;padding:.5rem 0">No dates blocked.</p>
|
|
<?php else: ?>
|
|
<?php foreach ($blocked as $bl): ?>
|
|
<div class="block-item" id="blocked-<?= $bl['id'] ?>">
|
|
<button class="del-btn" onclick="unblockDate(<?= $bl['id'] ?>)" title="Remove">✕</button>
|
|
<strong><?= date('M j, Y', strtotime($bl['block_date'])) ?></strong>
|
|
<?php if ($bl['reason']): ?><span style="color:#6b7280">— <?= htmlspecialchars($bl['reason']) ?></span><?php endif; ?>
|
|
</div>
|
|
<?php endforeach; ?>
|
|
<?php endif; ?>
|
|
</div>
|
|
</div>
|
|
|
|
</div><!-- /.main -->
|
|
|
|
<script>
|
|
// ── Status filter (client-side — no page navigation) ─────────────────────────
|
|
function filterBookings(status, btn) {
|
|
document.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('active'));
|
|
if (btn) btn.classList.add('active');
|
|
const rows = document.querySelectorAll('tr.booking-row, tr.detail-row');
|
|
rows.forEach(row => {
|
|
const show = status === 'all' || row.dataset.status === status;
|
|
row.style.display = show ? '' : 'none';
|
|
});
|
|
}
|
|
|
|
// ── Expand/collapse detail panel ──────────────────────────────────────────────
|
|
function toggleDetail(id) {
|
|
const row = document.getElementById('detail-' + id);
|
|
const btn = document.getElementById('expand-' + id);
|
|
const open = row.style.display !== 'table-row';
|
|
row.style.display = open ? 'table-row' : 'none';
|
|
row.classList.toggle('open', open);
|
|
btn.classList.toggle('open', open);
|
|
}
|
|
|
|
// ── Status dropdown ───────────────────────────────────────────────────────────
|
|
function updateStatus(sel) {
|
|
fetch('/admin/', {
|
|
method:'POST',
|
|
headers:{'Content-Type':'application/x-www-form-urlencoded','X-Requested-With':'XMLHttpRequest'},
|
|
body:'action=update_status&id='+sel.dataset.id+'&status='+sel.value
|
|
}).then(r=>r.json()).then(d=>{ if(!d.ok) alert('Error saving status'); });
|
|
}
|
|
|
|
// ── Toggle requirement (insurance / deposit / license) ────────────────────────
|
|
function toggleReq(id, field, btn) {
|
|
btn.disabled = true;
|
|
fetch('/admin/', {
|
|
method:'POST',
|
|
headers:{'Content-Type':'application/x-www-form-urlencoded','X-Requested-With':'XMLHttpRequest'},
|
|
body:'action=toggle_requirement&id='+id+'&field='+field
|
|
})
|
|
.then(r=>r.json())
|
|
.then(d=>{
|
|
if (!d.ok) { alert('Error saving'); btn.disabled=false; return; }
|
|
const on = d.value === 1;
|
|
// Update button
|
|
btn.classList.toggle('active', on);
|
|
const labels = {
|
|
insurance_verified: ['Mark Received','✓ Marked Received'],
|
|
deposit_received: ['Mark Deposit Received','✓ Deposit Received'],
|
|
license_verified: ['Mark License Verified','✓ License Verified'],
|
|
};
|
|
btn.textContent = on ? labels[field][1] : labels[field][0];
|
|
btn.disabled = false;
|
|
// Update icon
|
|
const icon = document.getElementById('icon-'+id+'-'+field);
|
|
if (icon) {
|
|
icon.className = 'flow-icon ' + (on ? 'done' : 'pending');
|
|
icon.textContent = on ? '✓' : icon.textContent.replace('✓','').trim() || '?';
|
|
// Restore step numbers for known fields
|
|
const stepNums = {insurance_verified:'4', deposit_received:'5', license_verified:'6'};
|
|
if (!on) icon.textContent = stepNums[field] || '?';
|
|
}
|
|
// Update meta text
|
|
const meta = document.getElementById('meta-'+id+'-'+field);
|
|
const metaTexts = {
|
|
insurance_verified: ['Pending — verify at pickup','Verified — on file'],
|
|
deposit_received: ['Pending — collect at pickup','Deposit received'],
|
|
license_verified: ['Verify at pickup','License verified'],
|
|
};
|
|
if (meta && metaTexts[field]) meta.textContent = on ? metaTexts[field][1] : metaTexts[field][0];
|
|
// Update progress dot in table row
|
|
const dot = document.getElementById('dot-'+id+'-'+field);
|
|
if (dot) {
|
|
dot.className = 'dot ' + (on ? 'dot-done' : 'dot-pending');
|
|
}
|
|
// Auto-uncheck reminder checkbox if item is now done
|
|
const reminderBox = document.getElementById('reminder-'+id);
|
|
if (reminderBox && on) {
|
|
const fieldToItem = {insurance_verified:'insurance', deposit_received:'deposit', license_verified:'license'};
|
|
const cb = reminderBox.querySelector('input[value="'+fieldToItem[field]+'"]');
|
|
if (cb) cb.checked = false;
|
|
}
|
|
});
|
|
}
|
|
|
|
// ── Admin notes ───────────────────────────────────────────────────────────────
|
|
function saveNotes(id) {
|
|
const ta = document.getElementById('notes-' + id);
|
|
const btn = ta.nextElementSibling;
|
|
fetch('/admin/', {
|
|
method:'POST',
|
|
headers:{'Content-Type':'application/x-www-form-urlencoded','X-Requested-With':'XMLHttpRequest'},
|
|
body:'action=save_admin_notes&id='+id+'¬es='+encodeURIComponent(ta.value)
|
|
}).then(r=>r.json()).then(d=>{
|
|
btn.textContent = d.ok ? 'Saved ✓' : 'Error';
|
|
setTimeout(()=>btn.textContent='Save Notes', 1800);
|
|
});
|
|
}
|
|
|
|
// ── Send reminder email ───────────────────────────────────────────────────────
|
|
function sendReminder(id) {
|
|
const box = document.getElementById('reminder-' + id);
|
|
const btn = document.getElementById('remind-btn-' + id);
|
|
const status= document.getElementById('remind-status-' + id);
|
|
const checks= Array.from(box.querySelectorAll('input[type=checkbox]:checked')).map(c=>c.value);
|
|
if (!checks.length) { alert('Please select at least one item to include in the reminder.'); return; }
|
|
if (!confirm('Send reminder email about: ' + checks.join(', ') + '?')) return;
|
|
btn.disabled = true;
|
|
btn.textContent = 'Sending…';
|
|
fetch('/admin/', {
|
|
method:'POST',
|
|
headers:{'Content-Type':'application/x-www-form-urlencoded','X-Requested-With':'XMLHttpRequest'},
|
|
body:'action=send_reminder&id='+id+'&items='+checks.join(',')
|
|
}).then(r=>r.json()).then(d=>{
|
|
btn.disabled = false;
|
|
if (d.ok) {
|
|
btn.textContent = 'Reminder Sent ✓';
|
|
btn.style.background = '#16a34a';
|
|
status.style.display = 'block';
|
|
status.style.color = '#16a34a';
|
|
status.textContent = 'Email sent successfully.';
|
|
setTimeout(()=>{
|
|
btn.textContent = 'Send Reminder Email';
|
|
btn.style.background = '';
|
|
status.style.display = 'none';
|
|
}, 4000);
|
|
} else {
|
|
btn.textContent = 'Send Reminder Email';
|
|
alert(d.error || 'Failed to send reminder.');
|
|
}
|
|
});
|
|
}
|
|
|
|
// ── Square payment actions (capture / void / refund) ─────────────────────────
|
|
function squareAction(id, action, btn) {
|
|
const labels = {
|
|
square_capture: ['Capture', 'Capturing…', 'Captured ✓'],
|
|
square_void: ['Void Hold', 'Voiding…', 'Voided ✓'],
|
|
square_refund: ['Refund', 'Refunding…', 'Refunded ✓'],
|
|
};
|
|
const [orig, working, done] = labels[action];
|
|
const confirmMsg = {
|
|
square_capture: 'Charge the $<?= number_format(DEPOSIT_AMOUNT,2) ?> deposit hold to this card?',
|
|
square_void: 'Void the $<?= number_format(DEPOSIT_AMOUNT,2) ?> deposit hold? The customer will NOT be charged.',
|
|
square_refund: 'Refund the deposit to this card?',
|
|
}[action];
|
|
if (!confirm(confirmMsg)) return;
|
|
btn.disabled = true;
|
|
btn.textContent = working;
|
|
fetch('/admin/', {
|
|
method:'POST',
|
|
headers:{'Content-Type':'application/x-www-form-urlencoded','X-Requested-With':'XMLHttpRequest'},
|
|
body:'action='+action+'&id='+id
|
|
}).then(r=>r.json()).then(d=>{
|
|
if (d.ok) {
|
|
btn.textContent = done;
|
|
btn.style.background = '#16a34a';
|
|
btn.style.color = '#fff';
|
|
btn.style.border = 'none';
|
|
// Update meta text and dot
|
|
const meta = document.getElementById('meta-'+id+'-deposit_received');
|
|
const icon = document.getElementById('icon-'+id+'-deposit_received');
|
|
const dot = document.getElementById('dot-'+id+'-deposit_received');
|
|
if (d.status === 'COMPLETED') {
|
|
if (meta) meta.textContent = 'Captured — deposit charged';
|
|
if (icon) { icon.className='flow-icon done'; icon.textContent='✓'; }
|
|
if (dot) dot.className='dot dot-done';
|
|
// Replace action area with refund button
|
|
const area = document.getElementById('deposit-actions-'+id);
|
|
if (area) area.innerHTML = '<button class="flow-toggle" onclick="squareAction('+id+',\'square_refund\',this)" style="border-color:#2563eb;color:#2563eb">Refund Deposit</button>';
|
|
} else if (d.status === 'CANCELED') {
|
|
if (meta) meta.textContent = 'Hold voided — no charge';
|
|
if (icon) { icon.className='flow-icon skip'; icon.textContent='—'; }
|
|
if (dot) dot.className='dot dot-skip';
|
|
const area = document.getElementById('deposit-actions-'+id);
|
|
if (area) area.innerHTML = '';
|
|
} else if (d.status === 'REFUNDED') {
|
|
if (meta) meta.textContent = 'Refunded — deposit returned';
|
|
if (icon) { icon.className='flow-icon pending'; icon.textContent='↩'; }
|
|
if (dot) dot.className='dot dot-skip';
|
|
const area = document.getElementById('deposit-actions-'+id);
|
|
if (area) area.innerHTML = '';
|
|
}
|
|
} else {
|
|
btn.textContent = orig;
|
|
btn.disabled = false;
|
|
alert('Error: ' + (d.error || 'Unknown error'));
|
|
}
|
|
}).catch(()=>{
|
|
btn.textContent = orig;
|
|
btn.disabled = false;
|
|
alert('Request failed. Please try again.');
|
|
});
|
|
}
|
|
|
|
// ── Block / unblock dates ─────────────────────────────────────────────────────
|
|
function blockDate(e) {
|
|
e.preventDefault();
|
|
var date = document.getElementById('blockDate').value;
|
|
var reason = document.getElementById('blockReason').value;
|
|
fetch('/admin/', {
|
|
method:'POST',
|
|
headers:{'Content-Type':'application/x-www-form-urlencoded','X-Requested-With':'XMLHttpRequest'},
|
|
body:'action=block_date&date='+date+'&reason='+encodeURIComponent(reason)
|
|
}).then(r=>r.json()).then(d=>{ if(d.ok) location.reload(); else alert(d.error); });
|
|
}
|
|
function unblockDate(id) {
|
|
if (!confirm('Remove this blocked date?')) return;
|
|
fetch('/admin/', {
|
|
method:'POST',
|
|
headers:{'Content-Type':'application/x-www-form-urlencoded','X-Requested-With':'XMLHttpRequest'},
|
|
body:'action=unblock_date&id='+id
|
|
}).then(r=>r.json()).then(d=>{ if(d.ok) document.getElementById('blocked-'+id).remove(); });
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|