Add Square deposit payment integration

- Square Web Payments SDK card element in booking form
- Delayed-capture hold ($100) on booking submit — not charged until confirmed
- Live payment status field: Verifying card → Authorizing → Confirmed w/ hold ID
- Admin: Capture / Void / Refund actions for each booking
- square_payment_id returned in API response for frontend confirmation display

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-22 18:33:16 +00:00
parent 8f5362aa95
commit cca3129f6e
4 changed files with 324 additions and 35 deletions
+150 -6
View File
@@ -174,6 +174,65 @@ if ($isAjax) {
exit; 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') { if ($action === 'block_date') {
$date = $_POST['date'] ?? ''; $date = $_POST['date'] ?? '';
$reason = substr($_POST['reason'] ?? '', 0, 200); $reason = substr($_POST['reason'] ?? '', 0, 200);
@@ -581,23 +640,46 @@ textarea.notes-ta:focus{border-color:#f97316}
</div> </div>
</div> </div>
<!-- Step 5: Deposit --> <!-- 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-step">
<div class="flow-icon <?= $stepDeposit?'done':($cancelled?'skip':'pending') ?>" id="icon-<?= $bid ?>-deposit_received"> <div class="flow-icon <?= $depositIcon ?>" id="icon-<?= $bid ?>-deposit_received">
<?= $stepDeposit?'✓':($cancelled?'—':'5') ?> <?= $depositLabel ?>
</div> </div>
<div class="flow-body"> <div class="flow-body">
<span class="flow-label">Security Deposit Received</span> <span class="flow-label">Security Deposit — $<?= number_format(DEPOSIT_AMOUNT,0) ?></span>
<span class="flow-meta" id="meta-<?= $bid ?>-deposit_received"> <span class="flow-meta" id="meta-<?= $bid ?>-deposit_received">
<?= $stepDeposit?'Deposit received':($cancelled?'N/A':'Pending — collect at pickup') ?> <?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> </span>
<?php if (!$cancelled): ?> <?php if (!$cancelled): ?>
<div class="flow-action"> <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':'' ?>" <button class="flow-toggle <?= $stepDeposit?'active':'' ?>"
id="btn-<?= $bid ?>-deposit_received" id="btn-<?= $bid ?>-deposit_received"
onclick="toggleReq(<?= $bid ?>,'deposit_received',this)"> onclick="toggleReq(<?= $bid ?>,'deposit_received',this)">
<?= $stepDeposit?'✓ Deposit Received':'Mark Deposit Received' ?> <?= $stepDeposit?'✓ Deposit Received':'Mark Deposit Received' ?>
</button> </button>
<?php endif; ?>
</div> </div>
<?php endif; ?> <?php endif; ?>
</div> </div>
@@ -831,6 +913,68 @@ function sendReminder(id) {
}); });
} }
// ── 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 deposit hold to this card?',
square_void: 'Void the deposit hold? The customer will NOT be charged.',
square_refund: 'Refund the full 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 ───────────────────────────────────────────────────── // ── Block / unblock dates ─────────────────────────────────────────────────────
function blockDate(e) { function blockDate(e) {
e.preventDefault(); e.preventDefault();
+46 -1
View File
@@ -16,6 +16,7 @@ $phone = trim(strip_tags($input['phone'] ?? ''));
$package = trim(strip_tags($input['package'] ?? '')); $package = trim(strip_tags($input['package'] ?? ''));
$date = trim(strip_tags($input['date'] ?? '')); $date = trim(strip_tags($input['date'] ?? ''));
$message = trim(strip_tags($input['message'] ?? '')); $message = trim(strip_tags($input['message'] ?? ''));
$squareToken = trim($input['square_token'] ?? '');
if (!$name || !$email || !$package || !$date) { if (!$name || !$email || !$package || !$date) {
http_response_code(400); http_response_code(400);
@@ -113,7 +114,51 @@ $confirmHtml = "<div style='max-width:600px;margin:0 auto;font-family:Arial,sans
</div> </div>
</div>"; </div>";
// Square deposit authorization (delayed capture — hold only, not charged yet)
$depositStatus = null;
if ($squareToken) {
$sqResp = squareApi('POST', '/payments', [
'source_id' => $squareToken,
'idempotency_key' => $ref . '-dep-' . time(),
'amount_money' => ['amount' => (int)(DEPOSIT_AMOUNT * 100), 'currency' => 'USD'],
'autocomplete' => false, // hold only — capture when confirmed
'location_id' => SQUARE_LOCATION_ID,
'note' => "Deposit hold — booking {$ref}",
'reference_id' => $ref,
'buyer_email_address' => $email,
]);
if (!empty($sqResp['payment']['id'])) {
$sqId = $sqResp['payment']['id'];
$sqSts = $sqResp['payment']['status']; // APPROVED
db()->prepare("UPDATE bookings SET square_payment_id=?, square_payment_status=? WHERE booking_ref=?")
->execute([$sqId, $sqSts, $ref]);
$depositStatus = $sqSts;
}
}
$depositLine = $depositStatus
? "<p style='margin:4px 0;font-size:14px;color:#374151'><strong>Deposit Hold:</strong> \$" . number_format(DEPOSIT_AMOUNT, 2) . " authorized (not charged — released if booking is declined)</p>"
: '';
// Inject deposit line into confirmation email
$confirmHtml = str_replace(
"<p style='margin:4px 0;font-size:14px;color:#374151'><strong>Total:</strong> {$amountLabel}</p>",
"<p style='margin:4px 0;font-size:14px;color:#374151'><strong>Total:</strong> {$amountLabel}</p>{$depositLine}",
$confirmHtml
);
// Add deposit note to admin email if applicable
if ($depositStatus) {
$adminHtml = str_replace(
"<p style='margin-top:16px;font-size:13px;color:#9ca3af'>",
"<p style='margin-top:8px;font-size:13px;color:#16a34a;font-weight:700'>✓ \$" . number_format(DEPOSIT_AMOUNT, 2) . " deposit hold authorized (Square — not yet captured)</p><p style='margin-top:8px;font-size:13px;color:#9ca3af'>",
$adminHtml
);
}
sendEmail(ADMIN_EMAIL, 'Parker Slingshot Admin', "New Booking {$ref}: {$name}{$pkgLabel} on {$dateLabel}", $adminHtml); sendEmail(ADMIN_EMAIL, 'Parker Slingshot Admin', "New Booking {$ref}: {$name}{$pkgLabel} on {$dateLabel}", $adminHtml);
sendEmail($email, $name, "Booking Request {$ref} — Parker County Slingshot Rentals", $confirmHtml); sendEmail($email, $name, "Booking Request {$ref} — Parker County Slingshot Rentals", $confirmHtml);
echo json_encode(['success'=>true,'ref'=>$ref,'message'=>"Booking request received! Your reference is {$ref}. We'll be in touch shortly."]); $msg = "Booking request received! Your reference is {$ref}. We'll be in touch shortly.";
if ($depositStatus) $msg .= " A \$" . number_format(DEPOSIT_AMOUNT, 2) . " refundable deposit hold has been placed on your card.";
echo json_encode(['success'=>true,'ref'=>$ref,'deposit_held'=>(bool)$depositStatus,'square_payment_id'=>$sqId??null,'message'=>$msg]);
+24
View File
@@ -13,6 +13,12 @@ define('MAIL_FROM', 'noreply@parkerslingshotrentals.com');
define('MAIL_FROM_NAME', 'Parker County Slingshot Rentals'); define('MAIL_FROM_NAME', 'Parker County Slingshot Rentals');
define('ADMIN_EMAIL', 'info@parkerslingshotrentals.com'); define('ADMIN_EMAIL', 'info@parkerslingshotrentals.com');
define('SQUARE_ACCESS_TOKEN', 'EAAAl3FsAu_2ri8kZE_ENEyi2T_C8HXXm5XQFY6Lbnd8SX6FqYp8J_upUeXNYh7v');
define('SQUARE_APP_ID', 'sq0idp-YSM7BU9IVyOWSzpeP-0nzQ');
define('SQUARE_LOCATION_ID', 'L8GZYHYKE95CE');
define('SQUARE_VERSION', '2024-01-18');
define('DEPOSIT_AMOUNT', 100.00); // $100 refundable security deposit hold
define('PACKAGES', [ define('PACKAGES', [
'half-day' => ['label' => 'Half Day (4 hrs)', 'amount' => 99.00, 'days' => 0], 'half-day' => ['label' => 'Half Day (4 hrs)', 'amount' => 99.00, 'days' => 0],
'full-day' => ['label' => 'Full Day (8 hrs)', 'amount' => 169.00, 'days' => 0], 'full-day' => ['label' => 'Full Day (8 hrs)', 'amount' => 169.00, 'days' => 0],
@@ -31,6 +37,24 @@ function db(): PDO {
return $pdo; return $pdo;
} }
function squareApi(string $method, string $path, array $body = []): array {
$ch = curl_init('https://connect.squareup.com/v2' . $path);
$headers = [
'Authorization: Bearer ' . SQUARE_ACCESS_TOKEN,
'Content-Type: application/json',
'Square-Version: ' . SQUARE_VERSION,
];
$opts = [CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => $headers, CURLOPT_TIMEOUT => 30, CURLOPT_SSL_VERIFYPEER => false];
if ($method === 'POST') {
$opts[CURLOPT_POST] = true;
$opts[CURLOPT_POSTFIELDS] = $body ? json_encode($body) : '{}';
}
curl_setopt_array($ch, $opts);
$resp = curl_exec($ch);
curl_close($ch);
return json_decode($resp ?: '{}', true);
}
function generateRef(): string { function generateRef(): string {
return 'PSR-' . strtoupper(substr(uniqid(), -6)); return 'PSR-' . strtoupper(substr(uniqid(), -6));
} }
+88 -12
View File
@@ -811,7 +811,17 @@
</select> </select>
<input type="date" name="date" required /> <input type="date" name="date" required />
<textarea name="message" placeholder="Anything else we should know? (optional)"></textarea> <textarea name="message" placeholder="Anything else we should know? (optional)"></textarea>
<button type="submit">Send Booking Request</button>
<!-- Square deposit card -->
<div style="background:rgba(255,255,255,0.04);border:1px solid rgba(255,255,255,0.1);border-radius:8px;padding:1rem;margin-top:0.25rem">
<p style="font-size:0.78rem;font-weight:700;text-transform:uppercase;letter-spacing:1px;color:rgba(249,115,22,0.8);margin-bottom:0.5rem">Refundable Deposit — $100</p>
<p style="font-size:0.8rem;color:rgba(255,255,255,0.5);margin-bottom:0.85rem;line-height:1.5">A $100 hold will be placed on your card — <strong style="color:rgba(255,255,255,0.75)">not charged</strong> until your booking is confirmed. Released in full if declined or at return.</p>
<div id="card-container" style="min-height:44px"></div>
<p id="card-errors" style="color:#f87171;font-size:0.78rem;margin-top:0.4rem;display:none"></p>
<div id="deposit-status" style="display:none;margin-top:0.6rem;font-size:0.82rem;border-radius:6px;padding:0.5rem 0.75rem;line-height:1.5"></div>
</div>
<button type="submit" id="submitBtn">Submit Booking Request</button>
<div class="form-msg" id="formMsg"></div> <div class="form-msg" id="formMsg"></div>
</form> </form>
</div> </div>
@@ -834,6 +844,7 @@
<p style="margin-top:0.5rem;">Polaris Slingshot&reg; is a registered trademark of Polaris Inc. We are an independent rental operator.</p> <p style="margin-top:0.5rem;">Polaris Slingshot&reg; is a registered trademark of Polaris Inc. We are an independent rental operator.</p>
</footer> </footer>
<script src="https://web.squarecdn.com/v1/square.js"></script>
<script> <script>
// ── Mobile nav ─────────────────────────────────────────────────────────────── // ── Mobile nav ───────────────────────────────────────────────────────────────
const navToggle = document.getElementById('navToggle'); const navToggle = document.getElementById('navToggle');
@@ -973,16 +984,74 @@
}); });
} }
// ── Square Web Payments ───────────────────────────────────────────────────────
let squareCard = null;
async function initSquare() {
if (!window.Square) return;
const cardEl = document.getElementById('card-container');
if (cardEl) cardEl.style.display = '';
try {
const payments = Square.payments('sq0idp-YSM7BU9IVyOWSzpeP-0nzQ', 'L8GZYHYKE95CE');
squareCard = await payments.card({
style: {
'.input-container': { borderColor: 'rgba(255,255,255,0.12)', borderRadius: '6px' },
'.input-container.is-focus': { borderColor: '#f97316' },
'.input-container.is-error': { borderColor: '#ef4444' },
'input': { color: '#ffffff', fontSize: '15px' },
'.message-text': { color: '#f87171' },
}
});
await squareCard.attach('#card-container');
} catch (e) {
console.warn('Square init error:', e);
}
}
initSquare();
// ── Booking Form ───────────────────────────────────────────────────────────── // ── Booking Form ─────────────────────────────────────────────────────────────
document.getElementById('bookingForm').addEventListener('submit', async function(e) { document.getElementById('bookingForm').addEventListener('submit', async function(e) {
e.preventDefault(); e.preventDefault();
const form = this; const form = this;
const msg = document.getElementById('formMsg'); const msg = document.getElementById('formMsg');
const btn = form.querySelector('button[type="submit"]'); const btn = document.getElementById('submitBtn');
btn.textContent = 'Sending...'; const cardErrors = document.getElementById('card-errors');
const depositStatus = document.getElementById('deposit-status');
btn.textContent = 'Processing…';
btn.disabled = true; btn.disabled = true;
msg.className = 'form-msg'; msg.className = 'form-msg';
msg.style.display = 'none'; msg.style.display = 'none';
if (cardErrors) { cardErrors.style.display = 'none'; cardErrors.textContent = ''; }
function setDepStatus(text, type) {
if (!depositStatus) return;
if (!text) { depositStatus.style.display = 'none'; depositStatus.textContent = ''; return; }
depositStatus.textContent = text;
depositStatus.style.display = 'block';
const map = {
processing: { background: 'rgba(249,115,22,0.1)', color: 'rgba(249,115,22,0.95)', border: '1px solid rgba(249,115,22,0.25)' },
success: { background: 'rgba(22,163,74,0.15)', color: '#86efac', border: '1px solid rgba(22,163,74,0.35)' },
error: { background: 'rgba(239,68,68,0.1)', color: '#fca5a5', border: '1px solid rgba(239,68,68,0.25)' },
};
Object.assign(depositStatus.style, map[type] || {});
}
try {
let squareToken = null;
if (squareCard) {
setDepStatus('Verifying card…', 'processing');
const result = await squareCard.tokenize();
if (result.status !== 'OK') {
const errMsg = result.errors ? result.errors.map(x => x.message).join(', ') : 'Card error — please check your details.';
if (cardErrors) { cardErrors.textContent = errMsg; cardErrors.style.display = 'block'; }
setDepStatus('', '');
btn.textContent = 'Submit Booking Request';
btn.disabled = false;
return;
}
squareToken = result.token;
setDepStatus('Card verified — authorizing $100 deposit hold…', 'processing');
}
const data = { const data = {
name: form.querySelector('[name="name"]').value, name: form.querySelector('[name="name"]').value,
@@ -991,34 +1060,41 @@
package: form.querySelector('[name="package"]').value, package: form.querySelector('[name="package"]').value,
date: form.querySelector('[name="date"]').value, date: form.querySelector('[name="date"]').value,
message: form.querySelector('[name="message"]').value, message: form.querySelector('[name="message"]').value,
square_token: squareToken,
}; };
try { const res = await fetch('/contact.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) });
const res = await fetch('/contact.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
});
const json = await res.json(); const json = await res.json();
if (json.success) { if (json.success) {
if (json.deposit_held) {
const holdSuffix = json.square_payment_id ? ' · Confirmation: …' + json.square_payment_id.slice(-10).toUpperCase() : '';
setDepStatus('✓ $100 deposit hold authorized' + holdSuffix, 'success');
const cardEl = document.getElementById('card-container');
if (cardEl) cardEl.style.display = 'none';
} else {
setDepStatus('', '');
}
msg.className = 'form-msg success'; msg.className = 'form-msg success';
msg.innerHTML = json.message || 'Thanks! We received your request and will be in touch soon.'; msg.innerHTML = 'Booking request received! Your reference is <strong>' + json.ref + '</strong>. We\'ll be in touch shortly.';
msg.style.display = 'block'; msg.style.display = 'block';
form.reset(); form.reset();
if (squareCard) { squareCard.destroy(); squareCard = null; }
selectedDate = null; selectedDate = null;
if (dateInput) dateInput.min = new Date().toISOString().split('T')[0]; if (dateInput) dateInput.min = new Date().toISOString().split('T')[0];
btn.textContent = 'Request Sent!'; btn.textContent = 'Request Sent!';
btn.style.background = '#16a34a'; btn.style.background = '#16a34a';
// Refresh calendar to show newly booked date
loadCalendar(calMonth, calYear); loadCalendar(calMonth, calYear);
} else { } else {
throw new Error(json.error || 'Something went wrong.'); throw new Error(json.error || 'Something went wrong.');
} }
} catch (err) { } catch (err) {
setDepStatus('', '');
msg.className = 'form-msg error'; msg.className = 'form-msg error';
msg.textContent = err.message || 'Something went wrong. Please try again or call us directly.'; msg.textContent = err.message || 'Something went wrong. Please try again or call us directly.';
msg.style.display = 'block'; msg.style.display = 'block';
btn.textContent = 'Send Booking Request'; btn.textContent = 'Submit Booking Request';
btn.disabled = false; btn.disabled = false;
} }
}); });