mirror of
https://github.com/myronblair/parkerslingshot
synced 2026-06-30 17:50:22 -05:00
Add doc upload/view, resend confirmation, update email for license/insurance steps
This commit is contained in:
+126
-11
@@ -106,8 +106,8 @@ if ($isAjax) {
|
|||||||
],
|
],
|
||||||
'insurance' => [
|
'insurance' => [
|
||||||
'label' => 'Proof of Personal Auto 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.',
|
'detail' => 'You\'ll need to show proof of valid personal auto insurance at pickup. To speed things up, you can upload it now — a photo of your insurance card is fine.',
|
||||||
'cta' => '',
|
'cta' => "<div style='margin-top:10px'><a href='" . SITE_URL . "/upload-docs.php?ref={$ref}&type=insurance' style='display:inline-block;background:#f97316;color:#fff;text-decoration:none;padding:9px 20px;border-radius:6px;font-weight:700;font-size:13px'>Upload Insurance Card →</a></div>",
|
||||||
],
|
],
|
||||||
'deposit' => [
|
'deposit' => [
|
||||||
'label' => 'Balance Due at Pickup',
|
'label' => 'Balance Due at Pickup',
|
||||||
@@ -116,8 +116,8 @@ if ($isAjax) {
|
|||||||
],
|
],
|
||||||
'license' => [
|
'license' => [
|
||||||
'label' => "Valid Driver's 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.",
|
'detail' => "Please bring your valid driver's license to pickup — we'll verify it before you head out. You can also upload a photo in advance to keep things moving at arrival.",
|
||||||
'cta' => '',
|
'cta' => "<div style='margin-top:10px'><a href='" . SITE_URL . "/upload-docs.php?ref={$ref}&type=license' style='display:inline-block;background:#f97316;color:#fff;text-decoration:none;padding:9px 20px;border-radius:6px;font-weight:700;font-size:13px'>Upload License Photo →</a></div>",
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -345,6 +345,61 @@ if ($isAjax) {
|
|||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($action === 'update_email') {
|
||||||
|
$id = (int)($_POST['id'] ?? 0);
|
||||||
|
$email = trim($_POST['email'] ?? '');
|
||||||
|
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { echo json_encode(['error'=>'Invalid email']); exit; }
|
||||||
|
db()->prepare("UPDATE bookings SET email=? WHERE id=?")->execute([$email, $id]);
|
||||||
|
echo json_encode(['ok'=>true,'email'=>$email]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($action === 'resend_confirmation') {
|
||||||
|
$id = (int)($_POST['id'] ?? 0);
|
||||||
|
$email = trim($_POST['email'] ?? '');
|
||||||
|
$stmt = db()->prepare("SELECT * FROM bookings WHERE id=?");
|
||||||
|
$stmt->execute([$id]);
|
||||||
|
$b = $stmt->fetch();
|
||||||
|
if (!$b) { echo json_encode(['error'=>'Not found']); exit; }
|
||||||
|
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) $email = $b['email'];
|
||||||
|
$pkg = PACKAGES[$b['package']] ?? ['label'=>$b['package'],'price'=>$b['amount']];
|
||||||
|
$dateLabel = date('F j, Y', strtotime($b['rental_date']));
|
||||||
|
$ref = $b['booking_ref'];
|
||||||
|
$amtLabel = '$' . number_format((float)$b['amount'], 2);
|
||||||
|
$depLabel = '$' . number_format(DEPOSIT_AMOUNT, 2);
|
||||||
|
$balLabel = '$' . number_format(max(0, (float)$b['amount'] - DEPOSIT_AMOUNT), 2);
|
||||||
|
$confirmHtml = "<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:#0d0d0d'>Booking Confirmation</h2>
|
||||||
|
<p style='color:#374151'>Hey " . htmlspecialchars($b['name']) . ", here's a copy of your booking confirmation.</p>
|
||||||
|
<div style='background:#fff7ed;border:1px solid #fed7aa;border-radius:10px;padding:20px;margin:20px 0'>
|
||||||
|
<p style='margin:0 0 6px;font-size:13px;color:#9ca3af;text-transform:uppercase;letter-spacing:1px'>Booking Reference</p>
|
||||||
|
<p style='margin:0 0 16px;font-size:22px;font-weight:700;color:#f97316'>{$ref}</p>
|
||||||
|
<p style='margin:4px 0;font-size:14px;color:#374151'><strong>Package:</strong> " . htmlspecialchars($pkg['label']) . "</p>
|
||||||
|
<p style='margin:4px 0;font-size:14px;color:#374151'><strong>Date:</strong> {$dateLabel}</p>
|
||||||
|
<p style='margin:4px 0;font-size:14px;color:#374151'><strong>Total:</strong> {$amtLabel}</p>
|
||||||
|
<p style='margin:4px 0;font-size:14px;color:#374151'><strong>Deposit hold:</strong> {$depLabel}</p>
|
||||||
|
<p style='margin:4px 0;font-size:14px;color:#374151'><strong>Balance at pickup:</strong> <span style='font-weight:700;color:#16a34a'>{$balLabel}</span></p>
|
||||||
|
</div>
|
||||||
|
<div style='margin:20px 0;padding:16px;background:#fff7ed;border:1px solid #fed7aa;border-radius:10px;text-align:center'>
|
||||||
|
<p style='margin:0 0 10px;font-size:14px;font-weight:700;color:#111'>Sign Your Rental Agreement</p>
|
||||||
|
<a href='" . SITE_URL . "/waiver.php?ref={$ref}' style='display:inline-block;background:#f97316;color:#fff;text-decoration:none;padding:10px 24px;border-radius:6px;font-weight:700;font-size:14px'>Sign Rental Agreement →</a>
|
||||||
|
</div>
|
||||||
|
<p style='color:#374151'>Questions? Call or text <strong>" . ADMIN_PHONE . "</strong> or reply to this email.</p>
|
||||||
|
<p style='color:#374151'>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($email, $b['name'], "Booking Confirmation {$ref} — Parker County Slingshot Rentals", $confirmHtml);
|
||||||
|
echo json_encode($sent ? ['ok'=>true,'email'=>$email] : ['error'=>'Email send failed — check Mailjet credentials']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
if ($action === 'customer_save') {
|
if ($action === 'customer_save') {
|
||||||
$cid = (int)($_POST['id'] ?? 0);
|
$cid = (int)($_POST['id'] ?? 0);
|
||||||
$name = substr(trim($_POST['name'] ?? ''), 0, 150);
|
$name = substr(trim($_POST['name'] ?? ''), 0, 150);
|
||||||
@@ -642,8 +697,10 @@ textarea.notes-ta:focus{border-color:#f97316}
|
|||||||
$stepConfirmed = in_array($b['status'], ['confirmed','completed']);
|
$stepConfirmed = in_array($b['status'], ['confirmed','completed']);
|
||||||
$stepWaiver = (bool)$b['waiver_signed'];
|
$stepWaiver = (bool)$b['waiver_signed'];
|
||||||
$stepInsurance = (bool)$b['insurance_verified'];
|
$stepInsurance = (bool)$b['insurance_verified'];
|
||||||
|
$insFile = $b['insurance_file'] ?? '';
|
||||||
$stepDeposit = (bool)$b['deposit_received'];
|
$stepDeposit = (bool)$b['deposit_received'];
|
||||||
$stepLicense = (bool)$b['license_verified'];
|
$stepLicense = (bool)$b['license_verified'];
|
||||||
|
$licFile = $b['license_file'] ?? '';
|
||||||
$stepHelmet = (bool)$b['helmet_provided'];
|
$stepHelmet = (bool)$b['helmet_provided'];
|
||||||
$stepSafety = (bool)$b['safety_course'];
|
$stepSafety = (bool)$b['safety_course'];
|
||||||
$stepOps = (bool)$b['operational_course'];
|
$stepOps = (bool)$b['operational_course'];
|
||||||
@@ -792,21 +849,27 @@ textarea.notes-ta:focus{border-color:#f97316}
|
|||||||
|
|
||||||
<!-- Step 4: Insurance -->
|
<!-- Step 4: Insurance -->
|
||||||
<div class="flow-step">
|
<div class="flow-step">
|
||||||
<div class="flow-icon <?= $stepInsurance?'done':($cancelled?'skip':'pending') ?>" id="icon-<?= $bid ?>-insurance_verified">
|
<div class="flow-icon <?= $stepInsurance?'done':($cancelled?'skip':($insFile?'pending':'pending')) ?>" id="icon-<?= $bid ?>-insurance_verified">
|
||||||
<?= $stepInsurance?'✓':($cancelled?'—':'4') ?>
|
<?= $stepInsurance?'✓':($cancelled?'—':'4') ?>
|
||||||
</div>
|
</div>
|
||||||
<div class="flow-body">
|
<div class="flow-body">
|
||||||
<span class="flow-label">Proof of Insurance Received</span>
|
<span class="flow-label">Proof of Insurance Received</span>
|
||||||
<span class="flow-meta" id="meta-<?= $bid ?>-insurance_verified">
|
<span class="flow-meta" id="meta-<?= $bid ?>-insurance_verified">
|
||||||
<?= $stepInsurance?'Verified — on file':($cancelled?'N/A':'Pending — verify at pickup') ?>
|
<?= $stepInsurance?'Verified — on file':($cancelled?'N/A':($insFile?'Doc submitted — verify at pickup':'Pending — verify at pickup')) ?>
|
||||||
</span>
|
</span>
|
||||||
|
<?php if ($insFile && !$cancelled): ?>
|
||||||
|
<a class="flow-link" href="/view-doc.php?ref=<?= urlencode($b['booking_ref']) ?>&type=insurance&_t=<?= $token ?>" target="_blank" style="margin-right:8px">📄 View Submitted Doc ↗</a>
|
||||||
|
<?php endif; ?>
|
||||||
<?php if (!$cancelled): ?>
|
<?php if (!$cancelled): ?>
|
||||||
<div class="flow-action">
|
<div class="flow-action" style="margin-top:4px">
|
||||||
<button class="flow-toggle <?= $stepInsurance?'active':'' ?>"
|
<button class="flow-toggle <?= $stepInsurance?'active':'' ?>"
|
||||||
id="btn-<?= $bid ?>-insurance_verified"
|
id="btn-<?= $bid ?>-insurance_verified"
|
||||||
onclick="toggleReq(<?= $bid ?>,'insurance_verified',this)">
|
onclick="toggleReq(<?= $bid ?>,'insurance_verified',this)">
|
||||||
<?= $stepInsurance?'✓ Marked Received':'Mark Received' ?>
|
<?= $stepInsurance?'✓ Verified at Pickup':'Mark Verified at Pickup' ?>
|
||||||
</button>
|
</button>
|
||||||
|
<?php if (!$insFile): ?>
|
||||||
|
<button class="flow-toggle" style="margin-left:6px" onclick="copyUploadLink('<?= htmlspecialchars($b['booking_ref']) ?>','insurance',this)">📎 Copy Upload Link</button>
|
||||||
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
@@ -820,15 +883,21 @@ textarea.notes-ta:focus{border-color:#f97316}
|
|||||||
<div class="flow-body">
|
<div class="flow-body">
|
||||||
<span class="flow-label">Driver's License Verified</span>
|
<span class="flow-label">Driver's License Verified</span>
|
||||||
<span class="flow-meta" id="meta-<?= $bid ?>-license_verified">
|
<span class="flow-meta" id="meta-<?= $bid ?>-license_verified">
|
||||||
<?= $stepLicense?'License verified':($cancelled?'N/A':'Verify at pickup') ?>
|
<?= $stepLicense?'Verified at pickup':($cancelled?'N/A':($licFile?'Doc submitted — verify at pickup':'Verify at pickup')) ?>
|
||||||
</span>
|
</span>
|
||||||
|
<?php if ($licFile && !$cancelled): ?>
|
||||||
|
<a class="flow-link" href="/view-doc.php?ref=<?= urlencode($b['booking_ref']) ?>&type=license&_t=<?= $token ?>" target="_blank" style="margin-right:8px">📄 View Submitted Doc ↗</a>
|
||||||
|
<?php endif; ?>
|
||||||
<?php if (!$cancelled): ?>
|
<?php if (!$cancelled): ?>
|
||||||
<div class="flow-action">
|
<div class="flow-action" style="margin-top:4px">
|
||||||
<button class="flow-toggle <?= $stepLicense?'active':'' ?>"
|
<button class="flow-toggle <?= $stepLicense?'active':'' ?>"
|
||||||
id="btn-<?= $bid ?>-license_verified"
|
id="btn-<?= $bid ?>-license_verified"
|
||||||
onclick="toggleReq(<?= $bid ?>,'license_verified',this)">
|
onclick="toggleReq(<?= $bid ?>,'license_verified',this)">
|
||||||
<?= $stepLicense?'✓ License Verified':'Mark License Verified' ?>
|
<?= $stepLicense?'✓ Verified at Pickup':'Mark Verified at Pickup' ?>
|
||||||
</button>
|
</button>
|
||||||
|
<?php if (!$licFile): ?>
|
||||||
|
<button class="flow-toggle" style="margin-left:6px" onclick="copyUploadLink('<?= htmlspecialchars($b['booking_ref']) ?>','license',this)">📎 Copy Upload Link</button>
|
||||||
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
@@ -1034,6 +1103,16 @@ textarea.notes-ta:focus{border-color:#f97316}
|
|||||||
<strong style="color:#111;display:block;margin-bottom:.35rem">Waiver Link</strong>
|
<strong style="color:#111;display:block;margin-bottom:.35rem">Waiver Link</strong>
|
||||||
<code style="word-break:break-all;font-size:.72rem">https://parkerslingshot.epictravelexpeditions.com/waiver.php?ref=<?= htmlspecialchars($b['booking_ref']) ?></code>
|
<code style="word-break:break-all;font-size:.72rem">https://parkerslingshot.epictravelexpeditions.com/waiver.php?ref=<?= htmlspecialchars($b['booking_ref']) ?></code>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div style="margin-top:1.25rem;padding:1rem;background:#f9fafb;border-radius:8px">
|
||||||
|
<strong style="color:#111;font-size:.8rem;display:block;margin-bottom:.5rem">Resend Confirmation Email</strong>
|
||||||
|
<div style="display:flex;gap:.5rem;align-items:center;flex-wrap:wrap">
|
||||||
|
<input type="email" id="resend-email-<?= $bid ?>" value="<?= htmlspecialchars($b['email']) ?>"
|
||||||
|
style="flex:1;min-width:180px;font-size:.82rem;padding:.45rem .65rem;border:1px solid #d1d5db;border-radius:6px;font-family:inherit" />
|
||||||
|
<button class="flow-toggle" onclick="resendConfirmation(<?= $bid ?>,this)" style="white-space:nowrap">Resend</button>
|
||||||
|
</div>
|
||||||
|
<div id="resend-status-<?= $bid ?>" style="margin-top:.4rem;font-size:.75rem;display:none"></div>
|
||||||
|
</div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -1586,6 +1665,42 @@ function saveNotes(id) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── Send reminder email ───────────────────────────────────────────────────────
|
// ── Send reminder email ───────────────────────────────────────────────────────
|
||||||
|
function copyUploadLink(ref, type, btn) {
|
||||||
|
const url = 'https://parkerslingshot.epictravelexpeditions.com/upload-docs.php?ref=' + encodeURIComponent(ref) + '&type=' + type;
|
||||||
|
navigator.clipboard.writeText(url).then(() => {
|
||||||
|
const orig = btn.textContent;
|
||||||
|
btn.textContent = '✓ Copied!';
|
||||||
|
setTimeout(() => btn.textContent = orig, 2000);
|
||||||
|
}).catch(() => prompt('Copy this link:', url));
|
||||||
|
}
|
||||||
|
|
||||||
|
function resendConfirmation(id, btn) {
|
||||||
|
const emailInput = document.getElementById('resend-email-' + id);
|
||||||
|
const status = document.getElementById('resend-status-' + id);
|
||||||
|
const email = emailInput ? emailInput.value.trim() : '';
|
||||||
|
if (!email) { alert('Enter an email address.'); return; }
|
||||||
|
if (!confirm('Resend booking confirmation to ' + email + '?')) return;
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.textContent = 'Sending…';
|
||||||
|
fetch('/admin/', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {'Content-Type':'application/x-www-form-urlencoded','X-Requested-With':'XMLHttpRequest'},
|
||||||
|
body: 'action=resend_confirmation&id=' + id + '&email=' + encodeURIComponent(email) + '&_t=' + ADMIN_TOKEN
|
||||||
|
}).then(r => r.json()).then(d => {
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.textContent = 'Resend';
|
||||||
|
if (d.ok) {
|
||||||
|
status.textContent = '✓ Sent to ' + d.email;
|
||||||
|
status.style.color = '#16a34a';
|
||||||
|
} else {
|
||||||
|
status.textContent = '✗ ' + (d.error || 'Failed');
|
||||||
|
status.style.color = '#dc2626';
|
||||||
|
}
|
||||||
|
status.style.display = 'block';
|
||||||
|
setTimeout(() => status.style.display = 'none', 4000);
|
||||||
|
}).catch(() => { btn.disabled = false; btn.textContent = 'Resend'; alert('Request failed.'); });
|
||||||
|
}
|
||||||
|
|
||||||
function sendReminder(id) {
|
function sendReminder(id) {
|
||||||
const box = document.getElementById('reminder-' + id);
|
const box = document.getElementById('reminder-' + id);
|
||||||
const btn = document.getElementById('remind-btn-' + id);
|
const btn = document.getElementById('remind-btn-' + id);
|
||||||
|
|||||||
+189
@@ -0,0 +1,189 @@
|
|||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/db.php';
|
||||||
|
|
||||||
|
header('X-Frame-Options: SAMEORIGIN');
|
||||||
|
header('X-Content-Type-Options: nosniff');
|
||||||
|
|
||||||
|
$ref = strtoupper(trim($_GET['ref'] ?? ''));
|
||||||
|
$type = in_array($_GET['type'] ?? '', ['license','insurance']) ? $_GET['type'] : '';
|
||||||
|
$error = '';
|
||||||
|
$done = false;
|
||||||
|
$booking = null;
|
||||||
|
|
||||||
|
if ($ref && $type) {
|
||||||
|
$stmt = db()->prepare("SELECT id, name, email, booking_ref, rental_date, status FROM bookings WHERE booking_ref=?");
|
||||||
|
$stmt->execute([$ref]);
|
||||||
|
$booking = $stmt->fetch();
|
||||||
|
if (!$booking) $error = 'Booking not found. Please check your confirmation email.';
|
||||||
|
elseif ($booking['status'] === 'cancelled') $error = 'This booking has been cancelled.';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'POST' && $booking && !$error) {
|
||||||
|
$file = $_FILES['doc'] ?? null;
|
||||||
|
if (!$file || $file['error'] !== UPLOAD_ERR_OK) {
|
||||||
|
$error = 'Upload failed — please try again or check file size.';
|
||||||
|
} else {
|
||||||
|
$finfo = new finfo(FILEINFO_MIME_TYPE);
|
||||||
|
$mime = $finfo->file($file['tmp_name']);
|
||||||
|
$allowed = ['image/jpeg','image/png','application/pdf'];
|
||||||
|
if (!in_array($mime, $allowed)) {
|
||||||
|
$error = 'Only JPG, PNG, or PDF files are accepted.';
|
||||||
|
} elseif ($file['size'] > 10 * 1024 * 1024) {
|
||||||
|
$error = 'File must be under 10 MB.';
|
||||||
|
} else {
|
||||||
|
$ext = ['image/jpeg'=>'jpg','image/png'=>'png','application/pdf'=>'pdf'][$mime];
|
||||||
|
$dir = __DIR__ . '/uploads/' . $ref;
|
||||||
|
if (!is_dir($dir)) mkdir($dir, 0750, true);
|
||||||
|
$fname = $type . '_' . date('YmdHis') . '.' . $ext;
|
||||||
|
$dest = $dir . '/' . $fname;
|
||||||
|
if (move_uploaded_file($file['tmp_name'], $dest)) {
|
||||||
|
$col = $type === 'license' ? 'license_file' : 'insurance_file';
|
||||||
|
$rel = 'uploads/' . $ref . '/' . $fname;
|
||||||
|
db()->prepare("UPDATE bookings SET {$col}=? WHERE booking_ref=?")->execute([$rel, $ref]);
|
||||||
|
|
||||||
|
$typeLabel = $type === 'license' ? "Driver's License" : 'Proof of Insurance';
|
||||||
|
$dateLabel = date('F j, Y', strtotime($booking['rental_date']));
|
||||||
|
$adminHtml = "<div style='font-family:Arial,sans-serif;max-width:560px;margin:0 auto'>
|
||||||
|
<div style='background:#f97316;padding:18px;text-align:center'>
|
||||||
|
<h1 style='color:#fff;margin:0;font-size:18px'>{$typeLabel} Uploaded — {$booking['booking_ref']}</h1>
|
||||||
|
</div>
|
||||||
|
<div style='padding:24px;background:#fff;border:1px solid #e5e7eb'>
|
||||||
|
<p><strong>" . htmlspecialchars($booking['name']) . "</strong> uploaded their <strong>{$typeLabel}</strong> for booking <strong>{$booking['booking_ref']}</strong> (rental: {$dateLabel}).</p>
|
||||||
|
<p style='margin-top:12px;font-size:13px;color:#6b7280'>View it in the admin panel under their booking detail.</p>
|
||||||
|
<div style='margin-top:16px'><a href='" . SITE_URL . "/admin/' style='display:inline-block;background:#f97316;color:#fff;text-decoration:none;padding:10px 22px;border-radius:6px;font-weight:700;font-size:13px'>Open Admin Panel →</a></div>
|
||||||
|
</div>
|
||||||
|
</div>";
|
||||||
|
sendEmail(ADMIN_EMAIL, 'Parker Slingshot Admin', "{$typeLabel} Uploaded — {$booking['booking_ref']}: " . $booking['name'], $adminHtml);
|
||||||
|
$done = true;
|
||||||
|
} else {
|
||||||
|
$error = 'Could not save file. Please try again.';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$typeLabel = $type === 'license' ? "Driver's License" : ($type === 'insurance' ? 'Proof of Insurance' : '');
|
||||||
|
$dateLabel = $booking ? date('F j, Y', strtotime($booking['rental_date'])) : '';
|
||||||
|
?><!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Upload Document — Parker County Slingshot Rentals</title>
|
||||||
|
<meta name="robots" content="noindex,nofollow" />
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Barlow+Condensed:wght@700;800&display=swap" rel="stylesheet" />
|
||||||
|
<style>
|
||||||
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
|
:root { --orange: #f97316; --black: #0d0d0d; }
|
||||||
|
body { font-family: 'Inter', sans-serif; background: #f3f4f6; color: #111; }
|
||||||
|
header { background: var(--black); padding: 1.25rem 2rem; display: flex; align-items: center; justify-content: space-between; }
|
||||||
|
header a { font-family: 'Barlow Condensed', sans-serif; font-size: 1.3rem; font-weight: 800; color: var(--orange); text-decoration: none; }
|
||||||
|
header span { font-size: 0.85rem; color: rgba(255,255,255,0.4); }
|
||||||
|
.wrap { max-width: 560px; margin: 2.5rem auto; padding: 0 1rem 4rem; }
|
||||||
|
.card { background: #fff; border-radius: 12px; box-shadow: 0 2px 12px rgba(0,0,0,0.08); padding: 2rem 2.5rem; }
|
||||||
|
@media (max-width: 560px) { .card { padding: 1.5rem; } }
|
||||||
|
h1 { font-family: 'Barlow Condensed', sans-serif; font-size: 1.9rem; font-weight: 800; margin-bottom: 0.25rem; }
|
||||||
|
.booking-badge { display: inline-block; background: #fff7ed; border: 1px solid #fed7aa; border-radius: 8px; padding: 0.6rem 1rem; margin: 1rem 0 1.5rem; }
|
||||||
|
.booking-badge .ref { font-size: 1.1rem; font-weight: 700; color: var(--orange); }
|
||||||
|
.booking-badge .meta { font-size: 0.82rem; color: #6b7280; margin-top: 2px; }
|
||||||
|
.upload-area { border: 2px dashed #d1d5db; border-radius: 10px; padding: 2rem; text-align: center; cursor: pointer; transition: border-color .2s, background .2s; background: #fafafa; position: relative; margin: 1rem 0; }
|
||||||
|
.upload-area:hover, .upload-area.drag { border-color: var(--orange); background: #fff7ed; }
|
||||||
|
.upload-area input[type=file] { position: absolute; inset: 0; opacity: 0; cursor: pointer; width: 100%; height: 100%; }
|
||||||
|
.upload-icon { font-size: 2.5rem; margin-bottom: 0.5rem; }
|
||||||
|
.upload-area p { font-size: 0.9rem; color: #6b7280; margin: 0; }
|
||||||
|
.upload-area .file-name { font-size: 0.88rem; color: var(--orange); font-weight: 600; margin-top: 0.5rem; display: none; }
|
||||||
|
.btn { display: block; width: 100%; background: var(--orange); color: #fff; border: none; border-radius: 8px; padding: 0.9rem; font-size: 1rem; font-weight: 700; cursor: pointer; transition: background .2s; margin-top: 1rem; }
|
||||||
|
.btn:hover { background: #ea580c; }
|
||||||
|
.btn:disabled { background: #d1d5db; cursor: not-allowed; }
|
||||||
|
.alert { padding: 0.85rem 1rem; border-radius: 8px; font-size: 0.9rem; margin-bottom: 1.25rem; }
|
||||||
|
.alert-error { background: rgba(239,68,68,.08); border: 1px solid rgba(239,68,68,.25); color: #dc2626; }
|
||||||
|
.success-icon { font-size: 3rem; text-align: center; margin-bottom: 1rem; }
|
||||||
|
.success-box { text-align: center; padding: .5rem 0; }
|
||||||
|
.success-box h1 { color: #16a34a; margin-bottom: 0.5rem; }
|
||||||
|
.success-box p { color: #374151; font-size: 0.95rem; max-width: 400px; margin: 0 auto .75rem; }
|
||||||
|
.hint { font-size: 0.8rem; color: #9ca3af; margin-top: 0.5rem; text-align: center; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header>
|
||||||
|
<a href="/">Parker County Slingshot Rentals</a>
|
||||||
|
<span>Document Upload</span>
|
||||||
|
</header>
|
||||||
|
<div class="wrap">
|
||||||
|
|
||||||
|
<?php if (!$ref || !$type || (!$booking && !$error)): ?>
|
||||||
|
<div class="card">
|
||||||
|
<h1>Upload Document</h1>
|
||||||
|
<p style="color:#6b7280;margin-top:.5rem">Invalid or missing upload link. Please use the link from your email or contact us.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php elseif ($error && !$booking): ?>
|
||||||
|
<div class="card">
|
||||||
|
<div class="alert alert-error"><?= htmlspecialchars($error) ?></div>
|
||||||
|
<p style="color:#6b7280;font-size:.9rem">Need help? Call or text <strong>(817) 555-0199</strong>.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php elseif ($done): ?>
|
||||||
|
<div class="card">
|
||||||
|
<div class="success-icon">✅</div>
|
||||||
|
<div class="success-box">
|
||||||
|
<h1>Upload Received!</h1>
|
||||||
|
<p>Thanks, <?= htmlspecialchars($booking['name']) ?>! Your <strong><?= htmlspecialchars($typeLabel) ?></strong> has been submitted for booking <strong><?= htmlspecialchars($booking['booking_ref']) ?></strong>.</p>
|
||||||
|
<p style="color:#6b7280;font-size:.85rem">We'll review it and still do a quick visual check at pickup. See you on <?= htmlspecialchars($dateLabel) ?>!</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php else: ?>
|
||||||
|
<?php if ($error): ?><div class="alert alert-error"><?= htmlspecialchars($error) ?></div><?php endif; ?>
|
||||||
|
<div class="card">
|
||||||
|
<h1>Upload <?= htmlspecialchars($typeLabel) ?></h1>
|
||||||
|
<div class="booking-badge">
|
||||||
|
<div class="ref"><?= htmlspecialchars($booking['booking_ref']) ?></div>
|
||||||
|
<div class="meta"><?= htmlspecialchars($booking['name']) ?> — <?= htmlspecialchars($dateLabel) ?></div>
|
||||||
|
</div>
|
||||||
|
<p style="color:#374151;font-size:.9rem;margin-bottom:.25rem">
|
||||||
|
<?php if ($type === 'insurance'): ?>
|
||||||
|
Please upload a photo or scan of your current auto insurance card. JPG, PNG, or PDF accepted (max 10 MB).
|
||||||
|
<?php else: ?>
|
||||||
|
Please upload a photo or scan of the front of your driver's license. JPG, PNG, or PDF accepted (max 10 MB).
|
||||||
|
<?php endif; ?>
|
||||||
|
</p>
|
||||||
|
<p style="color:#9ca3af;font-size:.8rem;margin-bottom:1rem">We'll still do a visual check at pickup — this is just for our records.</p>
|
||||||
|
<form method="post" enctype="multipart/form-data" id="uploadForm">
|
||||||
|
<div class="upload-area" id="dropZone">
|
||||||
|
<input type="file" name="doc" id="docInput" accept=".jpg,.jpeg,.png,.pdf" required />
|
||||||
|
<div class="upload-icon">📎</div>
|
||||||
|
<p>Tap or drag your file here</p>
|
||||||
|
<p style="font-size:.78rem;margin-top:4px">JPG • PNG • PDF • max 10 MB</p>
|
||||||
|
<div class="file-name" id="fileName"></div>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn" id="submitBtn">Upload <?= htmlspecialchars($typeLabel) ?></button>
|
||||||
|
</form>
|
||||||
|
<p class="hint">Your document is stored securely and only visible to Parker County Slingshot Rentals staff.</p>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
(function(){
|
||||||
|
const input = document.getElementById('docInput');
|
||||||
|
const label = document.getElementById('fileName');
|
||||||
|
const zone = document.getElementById('dropZone');
|
||||||
|
const btn = document.getElementById('submitBtn');
|
||||||
|
if (!input) return;
|
||||||
|
input.addEventListener('change', function() {
|
||||||
|
if (this.files[0]) {
|
||||||
|
label.textContent = this.files[0].name;
|
||||||
|
label.style.display = 'block';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
['dragover','dragenter'].forEach(e => zone.addEventListener(e, ev => { ev.preventDefault(); zone.classList.add('drag'); }));
|
||||||
|
['dragleave','drop'].forEach(e => zone.addEventListener(e, ev => zone.classList.remove('drag')));
|
||||||
|
document.getElementById('uploadForm')?.addEventListener('submit', function() {
|
||||||
|
if (btn) { btn.disabled = true; btn.textContent = 'Uploading…'; }
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,61 @@
|
|||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/db.php';
|
||||||
|
|
||||||
|
function _verifyToken(string $token): bool {
|
||||||
|
if (!preg_match('/^[a-f0-9]{64}$/', $token)) return false;
|
||||||
|
$stmt = db()->prepare("SELECT token FROM admin_tokens WHERE token=? AND expires_at > NOW()");
|
||||||
|
$stmt->execute([$token]);
|
||||||
|
return (bool)$stmt->fetch();
|
||||||
|
}
|
||||||
|
|
||||||
|
$token = preg_replace('/[^a-f0-9]/', '', $_GET['_t'] ?? '');
|
||||||
|
if (!_verifyToken($token)) {
|
||||||
|
http_response_code(403);
|
||||||
|
header('Content-Type: text/plain');
|
||||||
|
exit('Unauthorized — please log in to the admin panel first.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$ref = strtoupper(preg_replace('/[^A-Z0-9\-]/', '', $_GET['ref'] ?? ''));
|
||||||
|
$type = in_array($_GET['type'] ?? '', ['license','insurance']) ? $_GET['type'] : '';
|
||||||
|
if (!$ref || !$type) {
|
||||||
|
http_response_code(400);
|
||||||
|
header('Content-Type: text/plain');
|
||||||
|
exit('Missing parameters.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$col = $type === 'license' ? 'license_file' : 'insurance_file';
|
||||||
|
$stmt = db()->prepare("SELECT {$col} AS file_path FROM bookings WHERE booking_ref=?");
|
||||||
|
$stmt->execute([$ref]);
|
||||||
|
$row = $stmt->fetch();
|
||||||
|
|
||||||
|
if (!$row || !$row['file_path']) {
|
||||||
|
http_response_code(404);
|
||||||
|
header('Content-Type: text/plain');
|
||||||
|
exit('Document not found.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$base = realpath(__DIR__ . '/uploads');
|
||||||
|
$path = realpath(__DIR__ . '/' . $row['file_path']);
|
||||||
|
|
||||||
|
if (!$path || !$base || strpos($path, $base . DIRECTORY_SEPARATOR) !== 0) {
|
||||||
|
http_response_code(404);
|
||||||
|
header('Content-Type: text/plain');
|
||||||
|
exit('File not found.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$finfo = new finfo(FILEINFO_MIME_TYPE);
|
||||||
|
$mime = $finfo->file($path);
|
||||||
|
$allowed = ['image/jpeg' => 'jpg', 'image/png' => 'png', 'application/pdf' => 'pdf'];
|
||||||
|
if (!isset($allowed[$mime])) {
|
||||||
|
http_response_code(403);
|
||||||
|
header('Content-Type: text/plain');
|
||||||
|
exit('Invalid file type.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$fname = $type . '-' . $ref . '.' . $allowed[$mime];
|
||||||
|
header('Content-Type: ' . $mime);
|
||||||
|
header('Content-Disposition: inline; filename="' . $fname . '"');
|
||||||
|
header('Content-Length: ' . filesize($path));
|
||||||
|
header('Cache-Control: private, max-age=3600');
|
||||||
|
readfile($path);
|
||||||
|
exit;
|
||||||
Reference in New Issue
Block a user