mirror of
https://github.com/myronblair/parkerslingshotrentals
synced 2026-06-30 17:50:31 -05:00
Add per-customer booking flow checklist + fix admin login
Admin portal overhaul:
- Fix require_once path (was admin/db.php, should be ../db.php) — this was
the root cause of the login always redirecting back to the login page
- Fix session save path to /home/parkerslingshotrentals.com/sessions so the
web user (parke1909) can actually read sessions back (the system default
/var/lib/php/sessions was write-only for non-root)
- Fix AJAX unauthenticated response: return 401 JSON instead of login HTML
- Fresh bcrypt hash for admin password (Parker2026!)
- Add 3 new DB columns: insurance_verified, deposit_received, license_verified
- Replace flat bookings table with expandable per-customer flow panel:
click any row to open a 3-column detail drawer showing:
(1) full contact info + admin notes
(2) 6-step booking flow checklist with inline toggle buttons for steps
that admin marks (insurance, deposit, license)
(3) send-reminder email builder — pick which pending items to include,
send customer a personalized nudge with waiver link + instructions
- Progress dots in table row update live when admin toggles a step
- Stats row now includes waiver, insurance, deposit counts
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+582
-79
@@ -1,5 +1,8 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/db.php';
|
||||
require_once dirname(__DIR__) . '/db.php';
|
||||
ini_set('session.save_path', '/home/parkerslingshotrentals.com/sessions');
|
||||
ini_set('session.cookie_httponly', 1);
|
||||
ini_set('session.cookie_samesite', 'Lax');
|
||||
session_start();
|
||||
|
||||
$isAjax = !empty($_SERVER['HTTP_X_REQUESTED_WITH']) || (($_SERVER['HTTP_ACCEPT'] ?? '') === 'application/json');
|
||||
@@ -16,8 +19,14 @@ if ($_GET['action'] ?? '' === 'logout') {
|
||||
}
|
||||
$authed = !empty($_SESSION[ADMIN_SESSION_KEY]);
|
||||
|
||||
// ── AJAX handlers (must be authenticated) ────────────────────────────────────
|
||||
if ($isAjax && $authed) {
|
||||
// ── 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'] ?? '';
|
||||
|
||||
@@ -28,11 +37,10 @@ if ($isAjax && $authed) {
|
||||
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']);
|
||||
}
|
||||
} else { echo json_encode(['error'=>'Invalid']); }
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($action === 'save_admin_notes') {
|
||||
$id = (int)($_POST['id'] ?? 0);
|
||||
$notes = substr(trim($_POST['notes'] ?? ''), 0, 1000);
|
||||
@@ -40,6 +48,103 @@ if ($isAjax && $authed) {
|
||||
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' => 'Security Deposit',
|
||||
'detail' => 'A refundable security deposit is required at the time of pickup. Please have it ready — cash or card accepted. It will be returned in full 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 === 'block_date') {
|
||||
$date = $_POST['date'] ?? '';
|
||||
$reason = substr($_POST['reason'] ?? '', 0, 200);
|
||||
@@ -58,7 +163,7 @@ if ($isAjax && $authed) {
|
||||
exit;
|
||||
}
|
||||
|
||||
// ── Login page ────────────────────────────────────────────────────────────────
|
||||
// ── Login page ─────────────────────────────────────────────────────────────────
|
||||
if (!$authed) { ?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
@@ -95,7 +200,7 @@ button:hover{background:#ea580c}
|
||||
</html>
|
||||
<?php exit; }
|
||||
|
||||
// ── Dashboard data ────────────────────────────────────────────────────────────
|
||||
// ── Dashboard data ─────────────────────────────────────────────────────────────
|
||||
$statusFilter = $_GET['status'] ?? '';
|
||||
$where = $statusFilter ? "WHERE status = " . db()->quote($statusFilter) : '';
|
||||
$bookings = db()->query("SELECT * FROM bookings {$where} ORDER BY rental_date ASC, created_at DESC")->fetchAll();
|
||||
@@ -108,11 +213,12 @@ $stats = db()->query("
|
||||
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(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();
|
||||
|
||||
$statusColors = ['pending'=>'#d97706','confirmed'=>'#16a34a','completed'=>'#2563eb','cancelled'=>'#dc2626'];
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
@@ -122,40 +228,108 @@ $statusColors = ['pending'=>'#d97706','confirmed'=>'#16a34a','completed'=>'#2563
|
||||
<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}
|
||||
header h1{color:#f97316;font-size:1.2rem;font-weight:800}
|
||||
header a{color:rgba(255,255,255,.5);font-size:.85rem;text-decoration:none}
|
||||
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:1200px;margin:0 auto;padding:2rem 1.5rem}
|
||||
.stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:1rem;margin-bottom:2rem}
|
||||
.stat{background:#fff;border-radius:10px;padding:1.25rem;border:1px solid #e5e7eb;text-align:center}
|
||||
.stat-val{font-size:2rem;font-weight:800;line-height:1}
|
||||
.stat-lbl{font-size:.8rem;color:#6b7280;margin-top:.25rem;text-transform:uppercase;letter-spacing:.5px}
|
||||
.card{background:#fff;border-radius:10px;border:1px solid #e5e7eb;margin-bottom:2rem}
|
||||
.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 a{padding:.35rem .75rem;border-radius:6px;font-size:.8rem;text-decoration:none;color:#6b7280;border:1px solid #e5e7eb;margin-left:.35rem}
|
||||
.filters a.active,.filters a: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:.75rem;text-transform:uppercase;letter-spacing:.5px;border-bottom:1px solid #e5e7eb}
|
||||
td{padding:.75rem 1rem;border-bottom:1px solid #f3f4f6;vertical-align:top}
|
||||
tr:last-child td{border-bottom:none}
|
||||
tr:hover td{background:#fafafa}
|
||||
.badge{display:inline-block;padding:.2rem .6rem;border-radius:999px;font-size:.72rem;font-weight:700;text-transform:uppercase;letter-spacing:.5px}
|
||||
.badge-pending{background:#fef3c7;color:#92400e}
|
||||
.badge-confirmed{background:#dcfce7;color:#14532d}
|
||||
.badge-completed{background:#dbeafe;color:#1e3a8a}
|
||||
.badge-cancelled{background:#fee2e2;color:#7f1d1d}
|
||||
select.status-sel{font-size:.8rem;border:1px solid #e5e7eb;border-radius:6px;padding:.25rem .5rem;cursor:pointer}
|
||||
textarea.notes-ta{width:100%;font-size:.8rem;border:1px solid #e5e7eb;border-radius:6px;padding:.4rem .6rem;resize:vertical;min-height:50px;font-family:inherit}
|
||||
.save-btn{font-size:.75rem;background:#f97316;color:#fff;border:none;border-radius:5px;padding:.25rem .6rem;cursor:pointer;margin-top:.25rem}
|
||||
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}
|
||||
.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>
|
||||
@@ -168,11 +342,14 @@ textarea.notes-ta{width:100%;font-size:.8rem;border:1px solid #e5e7eb;border-rad
|
||||
|
||||
<!-- Stats -->
|
||||
<div class="stats">
|
||||
<div class="stat"><div class="stat-val"><?= $stats['total'] ?></div><div class="stat-lbl">Total Bookings</div></div>
|
||||
<div class="stat"><div class="stat-val" style="color:#d97706"><?= $stats['pending'] ?></div><div class="stat-lbl">Pending</div></div>
|
||||
<div class="stat"><div class="stat-val" style="color:#16a34a"><?= $stats['confirmed'] ?></div><div class="stat-lbl">Confirmed</div></div>
|
||||
<div class="stat"><div class="stat-val" style="color:#2563eb"><?= $stats['completed'] ?></div><div class="stat-lbl">Completed</div></div>
|
||||
<div class="stat"><div class="stat-val" style="color:#f97316">$<?= number_format($stats['revenue'],0) ?></div><div class="stat-lbl">Revenue</div></div>
|
||||
<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 -->
|
||||
@@ -180,13 +357,14 @@ textarea.notes-ta{width:100%;font-size:.8rem;border:1px solid #e5e7eb;border-rad
|
||||
<div class="card-header">
|
||||
<h2>Bookings</h2>
|
||||
<div class="filters">
|
||||
<a href="/admin/" class="<?= !$statusFilter ? 'active':'' ?>">All</a>
|
||||
<a href="/admin/?status=pending" class="<?= $statusFilter==='pending' ? 'active':'' ?>">Pending</a>
|
||||
<a href="/admin/?status=confirmed" class="<?= $statusFilter==='confirmed' ? 'active':'' ?>">Confirmed</a>
|
||||
<a href="/admin/?status=completed" class="<?= $statusFilter==='completed' ? 'active':'' ?>">Completed</a>
|
||||
<a href="/admin/?status=cancelled" class="<?= $statusFilter==='cancelled' ? 'active':'' ?>">Cancelled</a>
|
||||
<a href="/admin/" class="<?= !$statusFilter?'active':'' ?>">All</a>
|
||||
<a href="/admin/?status=pending" class="<?= $statusFilter==='pending' ?'active':'' ?>">Pending</a>
|
||||
<a href="/admin/?status=confirmed" class="<?= $statusFilter==='confirmed' ?'active':'' ?>">Confirmed</a>
|
||||
<a href="/admin/?status=completed" class="<?= $statusFilter==='completed' ?'active':'' ?>">Completed</a>
|
||||
<a href="/admin/?status=cancelled" class="<?= $statusFilter==='cancelled' ?'active':'' ?>">Cancelled</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if (empty($bookings)): ?>
|
||||
<div style="padding:3rem;text-align:center;color:#9ca3af">No bookings found.</div>
|
||||
<?php else: ?>
|
||||
@@ -194,52 +372,276 @@ textarea.notes-ta{width:100%;font-size:.8rem;border:1px solid #e5e7eb;border-rad
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Ref</th><th>Customer</th><th>Package</th><th>Date</th>
|
||||
<th>Amount</th><th>Status</th><th>Waiver</th><th>Admin Notes</th><th>Received</th>
|
||||
<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): ?>
|
||||
<tr>
|
||||
<td><strong><?= htmlspecialchars($b['booking_ref']) ?></strong></td>
|
||||
<td>
|
||||
<strong><?= htmlspecialchars($b['name']) ?></strong><br>
|
||||
<a href="mailto:<?= htmlspecialchars($b['email']) ?>" style="color:#f97316;font-size:.8rem"><?= htmlspecialchars($b['email']) ?></a>
|
||||
<?php if ($b['phone']): ?><br><span style="font-size:.8rem;color:#6b7280"><?= htmlspecialchars($b['phone']) ?></span><?php endif; ?>
|
||||
<?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" onclick="toggleDetail(<?= $bid ?>)">
|
||||
<td onclick="event.stopPropagation()">
|
||||
<button class="expand-btn" id="expand-<?= $bid ?>" onclick="toggleDetail(<?= $bid ?>)">►</button>
|
||||
</td>
|
||||
<td>
|
||||
<?php $pkg = PACKAGES[$b['package']] ?? ['label'=>$b['package']]; ?>
|
||||
<?= htmlspecialchars($pkg['label']) ?>
|
||||
<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:.8rem;color:#6b7280">→ <?= date('M j', strtotime($b['end_date'])) ?></span>
|
||||
<br><span style="font-size:.75rem;color:#9ca3af">→ <?= date('M j', strtotime($b['end_date'])) ?></span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td>$<?= number_format($b['amount'],2) ?></td>
|
||||
<td>
|
||||
<select class="status-sel" data-id="<?= $b['id'] ?>" onchange="updateStatus(this)">
|
||||
<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>
|
||||
<?php if ($b['waiver_signed']): ?>
|
||||
<span class="badge" style="background:#dcfce7;color:#14532d">✓ Signed</span>
|
||||
<br><span style="font-size:.72rem;color:#9ca3af"><?= date('M j g:ia', strtotime($b['waiver_signed_at'])) ?></span>
|
||||
<?php else: ?>
|
||||
<a href="https://parkerslingshotrentals.com/waiver.php?ref=<?= urlencode($b['booking_ref']) ?>" target="_blank"
|
||||
style="font-size:.78rem;color:#f97316;text-decoration:none">Send Link ↗</a>
|
||||
<br><span class="badge" style="background:#fef3c7;color:#92400e;margin-top:.25rem">Pending</span>
|
||||
<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>
|
||||
<textarea class="notes-ta" data-id="<?= $b['id'] ?>"><?= htmlspecialchars($b['admin_notes'] ?? '') ?></textarea>
|
||||
<button class="save-btn" onclick="saveNotes(this)">Save</button>
|
||||
<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 ?>">
|
||||
<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 -->
|
||||
<div class="flow-step">
|
||||
<div class="flow-icon <?= $stepDeposit?'done':($cancelled?'skip':'pending') ?>" id="icon-<?= $bid ?>-deposit_received">
|
||||
<?= $stepDeposit?'✓':($cancelled?'—':'5') ?>
|
||||
</div>
|
||||
<div class="flow-body">
|
||||
<span class="flow-label">Security Deposit Received</span>
|
||||
<span class="flow-meta" id="meta-<?= $bid ?>-deposit_received">
|
||||
<?= $stepDeposit?'Deposit received':($cancelled?'N/A':'Pending — collect at pickup') ?>
|
||||
</span>
|
||||
<?php if (!$cancelled): ?>
|
||||
<div class="flow-action">
|
||||
<button class="flow-toggle <?= $stepDeposit?'active':'' ?>"
|
||||
id="btn-<?= $bid ?>-deposit_received"
|
||||
onclick="toggleReq(<?= $bid ?>,'deposit_received',this)">
|
||||
<?= $stepDeposit?'✓ Deposit Received':'Mark Deposit Received' ?>
|
||||
</button>
|
||||
</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>
|
||||
<td style="font-size:.8rem;color:#9ca3af;white-space:nowrap"><?= date('M j g:ia', strtotime($b['created_at'])) ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
@@ -250,7 +652,7 @@ textarea.notes-ta{width:100%;font-size:.8rem;border:1px solid #e5e7eb;border-rad
|
||||
|
||||
<!-- Block Dates -->
|
||||
<div class="card">
|
||||
<div class="card-header"><h2>Block Dates (maintenance / personal use)</h2></div>
|
||||
<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>
|
||||
@@ -258,7 +660,7 @@ textarea.notes-ta{width:100%;font-size:.8rem;border:1px solid #e5e7eb;border-rad
|
||||
</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">
|
||||
<input type="text" id="blockReason" placeholder="e.g. Maintenance, personal use">
|
||||
</div>
|
||||
<button type="submit">Block Date</button>
|
||||
</form>
|
||||
@@ -276,9 +678,21 @@ textarea.notes-ta{width:100%;font-size:.8rem;border:1px solid #e5e7eb;border-rad
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div><!-- /.main -->
|
||||
|
||||
<script>
|
||||
// ── 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',
|
||||
@@ -287,18 +701,108 @@ function updateStatus(sel) {
|
||||
}).then(r=>r.json()).then(d=>{ if(!d.ok) alert('Error saving status'); });
|
||||
}
|
||||
|
||||
function saveNotes(btn) {
|
||||
var ta = btn.previousElementSibling;
|
||||
// ── 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=save_admin_notes&id='+ta.dataset.id+'¬es='+encodeURIComponent(ta.value)
|
||||
}).then(r=>r.json()).then(d=>{
|
||||
btn.textContent = d.ok ? 'Saved ✓' : 'Error';
|
||||
setTimeout(()=>btn.textContent='Save', 1500);
|
||||
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.');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ── Block / unblock dates ─────────────────────────────────────────────────────
|
||||
function blockDate(e) {
|
||||
e.preventDefault();
|
||||
var date = document.getElementById('blockDate').value;
|
||||
@@ -309,7 +813,6 @@ function blockDate(e) {
|
||||
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/', {
|
||||
|
||||
@@ -5,7 +5,7 @@ define('PARKER_DB_USER', 'parker_user');
|
||||
define('PARKER_DB_PASS', 'Pk4rk3r_2026!Tx');
|
||||
|
||||
define('ADMIN_USER', 'admin');
|
||||
define('ADMIN_PASS', '$2y$12$Tz8QkXv9mNpL3wR1uE6OaOQbKjH5sYdF2cVnMxPt7lGAeWqZiBSHu'); // Parker2026!
|
||||
define('ADMIN_PASS', '$2y$10$ynnk3RfarOD7VIJizC30kuXqu6tQ3gotNrlp5y33afh5fPOgnAMU6'); // Parker2026!
|
||||
define('ADMIN_SESSION_KEY', 'parker_admin_auth');
|
||||
|
||||
define('SENDGRID_API_KEY', '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');
|
||||
|
||||
Reference in New Issue
Block a user