mirror of
https://github.com/myronblair/parkerslingshotrentals
synced 2026-06-30 17:50:31 -05:00
Customer detail: expandable rows with full booking flow + payment controls
This commit is contained in:
+395
-20
@@ -231,7 +231,8 @@ if ($isAjax) {
|
||||
$reason = substr($_POST['reason'] ?? '', 0, 200);
|
||||
if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $date)) {
|
||||
db()->prepare("INSERT IGNORE INTO blocked_dates (block_date, reason) VALUES (?,?)")->execute([$date, $reason]);
|
||||
echo json_encode(['ok'=>true]);
|
||||
$newId = (int)db()->lastInsertId();
|
||||
echo json_encode(['ok'=>true, 'id'=>$newId, 'date'=>$date, 'reason'=>$reason]);
|
||||
} else { echo json_encode(['error'=>'Invalid date']); }
|
||||
exit;
|
||||
}
|
||||
@@ -318,10 +319,14 @@ button:hover{background:#ea580c}
|
||||
$bookings = db()->query("SELECT * FROM bookings ORDER BY rental_date ASC, created_at DESC")->fetchAll();
|
||||
$blocked = db()->query("SELECT * FROM blocked_dates ORDER BY block_date ASC")->fetchAll();
|
||||
$customers = db()->query("
|
||||
SELECT c.*,
|
||||
(SELECT COUNT(*) FROM bookings WHERE email=c.email) AS booking_count
|
||||
SELECT c.*
|
||||
FROM pcs_customers c ORDER BY c.created_at DESC
|
||||
")->fetchAll();
|
||||
// Group bookings by customer email in PHP (avoids cross-table collation issues)
|
||||
$bookingsByEmail = [];
|
||||
foreach ($bookings as $_b) {
|
||||
$bookingsByEmail[strtolower(trim($_b['email']))][] = $_b;
|
||||
}
|
||||
|
||||
$stats = db()->query("
|
||||
SELECT
|
||||
@@ -463,6 +468,18 @@ textarea.notes-ta:focus{border-color:#f97316}
|
||||
.cust-badge{display:inline-block;padding:.15rem .5rem;border-radius:999px;font-size:.7rem;font-weight:700}
|
||||
.cust-active{background:#dcfce7;color:#15803d}
|
||||
.cust-inactive{background:#f3f4f6;color:#9ca3af}
|
||||
|
||||
/* Customer expandable detail */
|
||||
.cust-detail-panel{display:grid;grid-template-columns:250px 1fr;border-top:2px solid #f97316}
|
||||
@media(max-width:900px){.cust-detail-panel{grid-template-columns:1fr}}
|
||||
.cdp-info{padding:1.5rem;border-right:1px solid #f3f4f6;background:#f9fafb}
|
||||
.cdp-info h3,.cdp-bookings h3{font-size:.72rem;font-weight:700;text-transform:uppercase;letter-spacing:1px;color:#9ca3af;margin-bottom:1rem}
|
||||
.cdp-bookings{padding:1.5rem;min-width:0}
|
||||
.cust-booking-card{border:1px solid #e5e7eb;border-radius:8px;margin-bottom:1.25rem;overflow:hidden}
|
||||
.cust-booking-card:last-child{margin-bottom:0}
|
||||
.cbc-header{display:flex;align-items:center;gap:.65rem;padding:.7rem 1rem;background:#f9fafb;border-bottom:1px solid #f3f4f6;flex-wrap:wrap}
|
||||
.cbc-flow{padding:.5rem 1rem;border-bottom:1px solid #f3f4f6}
|
||||
.cbc-notes{padding:.75rem 1rem}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -887,31 +904,241 @@ textarea.notes-ta:focus{border-color:#f97316}
|
||||
<table id="custTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:36px"></th>
|
||||
<th>Name</th>
|
||||
<th>Email</th>
|
||||
<th>Phone</th>
|
||||
<th>DOB</th>
|
||||
<th>Bookings</th>
|
||||
<th>Status</th>
|
||||
<th>Added</th>
|
||||
<th style="width:80px"></th>
|
||||
<th style="width:60px"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($customers as $c): ?>
|
||||
<tr class="cust-row" data-search="<?= strtolower(htmlspecialchars($c['name'].' '.$c['email'].' '.($c['phone']??''))) ?>">
|
||||
<?php foreach ($customers as $c):
|
||||
$cid = $c['id'];
|
||||
$custBookings = $bookingsByEmail[strtolower(trim($c['email']))] ?? [];
|
||||
?>
|
||||
<tr class="cust-row booking-row" data-cid="<?= $cid ?>"
|
||||
data-search="<?= strtolower(htmlspecialchars($c['name'].' '.$c['email'].' '.($c['phone']??''))) ?>"
|
||||
onclick="toggleCustDetail(<?= $cid ?>)">
|
||||
<td onclick="event.stopPropagation()">
|
||||
<button class="expand-btn" id="cexpand-<?= $cid ?>" onclick="toggleCustDetail(<?= $cid ?>)">►</button>
|
||||
</td>
|
||||
<td><strong><?= htmlspecialchars($c['name']) ?></strong></td>
|
||||
<td style="font-size:.82rem"><a href="mailto:<?= htmlspecialchars($c['email']) ?>" style="color:#f97316;text-decoration:none"><?= htmlspecialchars($c['email']) ?></a></td>
|
||||
<td style="font-size:.82rem"><a href="mailto:<?= htmlspecialchars($c['email']) ?>" onclick="event.stopPropagation()" style="color:#f97316;text-decoration:none"><?= htmlspecialchars($c['email']) ?></a></td>
|
||||
<td style="font-size:.82rem"><?= htmlspecialchars($c['phone'] ?? '—') ?></td>
|
||||
<td style="font-size:.82rem"><?= $c['dob'] ? date('M j, Y', strtotime($c['dob'])) : '—' ?></td>
|
||||
<td style="text-align:center"><?= (int)$c['booking_count'] ?></td>
|
||||
<td style="text-align:center"><?= count($custBookings) ?></td>
|
||||
<td><span class="cust-badge <?= $c['is_active']?'cust-active':'cust-inactive' ?>"><?= $c['is_active']?'Active':'Inactive' ?></span></td>
|
||||
<td style="font-size:.78rem;color:#9ca3af;white-space:nowrap"><?= date('M j, Y', strtotime($c['created_at'])) ?></td>
|
||||
<td style="white-space:nowrap">
|
||||
<button class="flow-toggle" style="font-size:.72rem;margin-right:4px"
|
||||
<td onclick="event.stopPropagation()" style="white-space:nowrap">
|
||||
<button class="flow-toggle" style="font-size:.72rem;margin-right:3px"
|
||||
onclick="editCustomer(<?= htmlspecialchars(json_encode($c), ENT_QUOTES) ?>)">Edit</button>
|
||||
<button class="flow-toggle" style="font-size:.72rem;border-color:#dc2626;color:#dc2626"
|
||||
onclick="deleteCustomer(<?= $c['id'] ?>,this)">Del</button>
|
||||
onclick="deleteCustomer(<?= $cid ?>,this)">Del</button>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- ── Customer Detail Row ──────────────────────────────────────────── -->
|
||||
<tr id="cdetail-<?= $cid ?>" style="display:none">
|
||||
<td colspan="8" style="padding:0">
|
||||
<div class="cust-detail-panel">
|
||||
|
||||
<!-- Left: Customer profile -->
|
||||
<div class="cdp-info">
|
||||
<h3>Customer Profile</h3>
|
||||
<div class="ci-name"><?= htmlspecialchars($c['name']) ?></div>
|
||||
<div class="ci-row"><a href="mailto:<?= htmlspecialchars($c['email']) ?>"><?= htmlspecialchars($c['email']) ?></a></div>
|
||||
<?php if ($c['phone']): ?>
|
||||
<div class="ci-row"><a href="tel:<?= htmlspecialchars($c['phone']) ?>"><?= htmlspecialchars($c['phone']) ?></a></div>
|
||||
<?php endif; ?>
|
||||
<?php if ($c['dob']): ?>
|
||||
<div class="ci-field">Date of Birth</div>
|
||||
<div style="font-size:.85rem"><?= date('F j, Y', strtotime($c['dob'])) ?></div>
|
||||
<?php endif; ?>
|
||||
<?php if ($c['address']): ?>
|
||||
<div class="ci-field">Address</div>
|
||||
<div style="font-size:.82rem;color:#6b7280"><?= nl2br(htmlspecialchars($c['address'])) ?></div>
|
||||
<?php endif; ?>
|
||||
<?php if ($c['notes']): ?>
|
||||
<div class="ci-field">Notes</div>
|
||||
<div style="font-size:.82rem;color:#6b7280;font-style:italic"><?= nl2br(htmlspecialchars($c['notes'])) ?></div>
|
||||
<?php endif; ?>
|
||||
<div style="margin-top:1.25rem;display:flex;gap:.5rem;align-items:center;flex-wrap:wrap">
|
||||
<span class="cust-badge <?= $c['is_active']?'cust-active':'cust-inactive' ?>"><?= $c['is_active']?'Active':'Inactive' ?></span>
|
||||
<button class="save-btn" style="font-size:.78rem"
|
||||
onclick="editCustomer(<?= htmlspecialchars(json_encode($c), ENT_QUOTES) ?>)">Edit Profile</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right: Booking history -->
|
||||
<div class="cdp-bookings">
|
||||
<h3>Booking History (<?= count($custBookings) ?>)</h3>
|
||||
<?php if (empty($custBookings)): ?>
|
||||
<p style="color:#9ca3af;font-size:.85rem">No bookings yet.</p>
|
||||
<?php else: ?>
|
||||
<?php foreach ($custBookings as $cb):
|
||||
$bid = $cb['id'];
|
||||
$pkg = PACKAGES[$cb['package']] ?? ['label'=>$cb['package']];
|
||||
$stepConfirmed = in_array($cb['status'], ['confirmed','completed']);
|
||||
$stepWaiver = (bool)$cb['waiver_signed'];
|
||||
$stepInsurance = (bool)$cb['insurance_verified'];
|
||||
$stepDeposit = (bool)$cb['deposit_received'];
|
||||
$stepLicense = (bool)$cb['license_verified'];
|
||||
$cancelled = $cb['status'] === 'cancelled';
|
||||
$sqStatus = $cb['square_payment_status'] ?? '';
|
||||
$sqId = $cb['square_payment_id'] ?? '';
|
||||
?>
|
||||
<div class="cust-booking-card">
|
||||
|
||||
<!-- Card header: ref, package, date, amount, status -->
|
||||
<div class="cbc-header">
|
||||
<span class="ci-ref" style="flex-shrink:0"><?= htmlspecialchars($cb['booking_ref']) ?></span>
|
||||
<span style="font-size:.84rem;font-weight:600;flex:1;min-width:0">
|
||||
<?= htmlspecialchars($pkg['label']) ?> · <?= date('M j, Y', strtotime($cb['rental_date'])) ?> · $<?= number_format($cb['amount'],2) ?>
|
||||
</span>
|
||||
<select class="status-sel" data-id="<?= $bid ?>" onchange="updateStatus(this)">
|
||||
<?php foreach (['pending','confirmed','completed','cancelled'] as $s): ?>
|
||||
<option value="<?= $s ?>" <?= $cb['status']===$s?'selected':'' ?>><?= ucfirst($s) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Booking flow -->
|
||||
<div class="cbc-flow">
|
||||
<div class="flow-list">
|
||||
|
||||
<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($cb['created_at'])) ?></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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">Booking Confirmed</span>
|
||||
<span class="flow-meta"><?= $stepConfirmed?('Confirmed — '.ucfirst($cb['status'])):($cancelled?'Cancelled':'Awaiting confirmation — change status above') ?></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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<?= $cb['waiver_signed_at']?' on '.date('M j g:ia',strtotime($cb['waiver_signed_at'])):''; ?>
|
||||
<?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="<?= SITE_URL ?>/waiver.php?ref=<?= urlencode($cb['booking_ref']) ?>" target="_blank">Open Waiver Link ↗</a>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flow-step">
|
||||
<div class="flow-icon <?= $stepInsurance?'done':($cancelled?'skip':'pending') ?>" id="cv-icon-<?= $bid ?>-insurance_verified">
|
||||
<?= $stepInsurance?'✓':($cancelled?'—':'4') ?>
|
||||
</div>
|
||||
<div class="flow-body">
|
||||
<span class="flow-label">Insurance Received</span>
|
||||
<span class="flow-meta" id="cv-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="cv-btn-<?= $bid ?>-insurance_verified"
|
||||
onclick="cvToggleReq(<?= $bid ?>,'insurance_verified',this)">
|
||||
<?= $stepInsurance?'✓ Marked Received':'Mark Received' ?>
|
||||
</button>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php
|
||||
$cvDepIcon = $cancelled ? 'skip' : (($stepDeposit||$sqStatus==='COMPLETED') ? 'done' : 'pending');
|
||||
$cvDepLabel = $cancelled ? '—' : (($stepDeposit||$sqStatus==='COMPLETED') ? '✓' : '5');
|
||||
?>
|
||||
<div class="flow-step">
|
||||
<div class="flow-icon <?= $cvDepIcon ?>" id="cv-icon-<?= $bid ?>-deposit_received">
|
||||
<?= $cvDepLabel ?>
|
||||
</div>
|
||||
<div class="flow-body">
|
||||
<span class="flow-label">Deposit & Balance — $<?= number_format(DEPOSIT_AMOUNT,2) ?> held · $<?= number_format($cb['amount']-DEPOSIT_AMOUNT,2) ?> at pickup</span>
|
||||
<span class="flow-meta" id="cv-meta-<?= $bid ?>-deposit_received">
|
||||
<?php if ($cancelled): ?>N/A
|
||||
<?php elseif ($sqStatus==='COMPLETED'): ?>Captured — $<?= number_format((float)($cb['deposit_paid']??DEPOSIT_AMOUNT),2) ?> charged
|
||||
<?php elseif ($sqStatus==='REFUNDED'): ?>Refunded — deposit returned
|
||||
<?php elseif ($sqStatus==='CANCELED'): ?>Hold voided — no charge
|
||||
<?php elseif ($sqStatus==='APPROVED'||$sqStatus==='PENDING'): ?>Hold active — card authorized, not yet charged
|
||||
<?php elseif ($stepDeposit): ?>Marked received (manual)
|
||||
<?php else: ?>Pending — no card on file
|
||||
<?php endif; ?>
|
||||
</span>
|
||||
<?php if (!$cancelled): ?>
|
||||
<div class="flow-action" id="cv-dep-actions-<?= $bid ?>">
|
||||
<?php if ($sqStatus==='APPROVED'||$sqStatus==='PENDING'): ?>
|
||||
<button class="flow-toggle" onclick="cvSquareAction(<?= $bid ?>,'square_capture',this)" style="margin-right:4px">Capture $<?= number_format(DEPOSIT_AMOUNT,0) ?></button>
|
||||
<button class="flow-toggle" onclick="cvSquareAction(<?= $bid ?>,'square_void',this)" style="border-color:#dc2626;color:#dc2626">Void Hold</button>
|
||||
<?php elseif ($sqStatus==='COMPLETED'): ?>
|
||||
<button class="flow-toggle" onclick="cvSquareAction(<?= $bid ?>,'square_refund',this)" style="border-color:#2563eb;color:#2563eb">Refund $<?= number_format((float)($cb['deposit_paid']??DEPOSIT_AMOUNT),2) ?></button>
|
||||
<?php elseif (!$sqId): ?>
|
||||
<button class="flow-toggle <?= $stepDeposit?'active':'' ?>" id="cv-btn-<?= $bid ?>-deposit_received"
|
||||
onclick="cvToggleReq(<?= $bid ?>,'deposit_received',this)">
|
||||
<?= $stepDeposit?'✓ Deposit Received':'Mark Deposit Received' ?>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flow-step">
|
||||
<div class="flow-icon <?= $stepLicense?'done':($cancelled?'skip':'pending') ?>" id="cv-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="cv-meta-<?= $bid ?>-license_verified">
|
||||
<?= $stepLicense?'Verified — on file':($cancelled?'N/A':'Verify at pickup') ?>
|
||||
</span>
|
||||
<?php if (!$cancelled): ?>
|
||||
<div class="flow-action">
|
||||
<button class="flow-toggle <?= $stepLicense?'active':'' ?>" id="cv-btn-<?= $bid ?>-license_verified"
|
||||
onclick="cvToggleReq(<?= $bid ?>,'license_verified',this)">
|
||||
<?= $stepLicense?'✓ License Verified':'Mark License Verified' ?>
|
||||
</button>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div><!-- /.flow-list -->
|
||||
</div><!-- /.cbc-flow -->
|
||||
|
||||
<!-- Admin notes -->
|
||||
<div class="cbc-notes">
|
||||
<div class="ci-field">Admin Notes</div>
|
||||
<textarea class="notes-ta" id="cv-notes-<?= $bid ?>"><?= htmlspecialchars($cb['admin_notes']??'') ?></textarea>
|
||||
<button class="save-btn" onclick="cvSaveNotes(<?= $bid ?>)" style="margin-top:.4rem">Save Notes</button>
|
||||
</div>
|
||||
|
||||
</div><!-- /.cust-booking-card -->
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</div><!-- /.cdp-bookings -->
|
||||
|
||||
</div><!-- /.cust-detail-panel -->
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
@@ -1182,7 +1409,7 @@ function saveCustomer() {
|
||||
}).then(r=>r.json()).then(d=>{
|
||||
if (d.ok) {
|
||||
showCustMsg(id === '0' ? 'Customer added.' : 'Saved.', '#16a34a');
|
||||
setTimeout(()=>location.reload(), 900);
|
||||
setTimeout(()=>{ location.href = '/admin/?_t=' + ADMIN_TOKEN; }, 900);
|
||||
} else {
|
||||
showCustMsg(d.error || 'Error saving.', 'red');
|
||||
}
|
||||
@@ -1204,8 +1431,139 @@ function deleteCustomer(id, btn) {
|
||||
|
||||
function filterCustomers(q) {
|
||||
const term = q.toLowerCase().trim();
|
||||
document.querySelectorAll('.cust-row').forEach(row=>{
|
||||
row.style.display = (!term || row.dataset.search.includes(term)) ? '' : 'none';
|
||||
document.querySelectorAll('tr.cust-row').forEach(row => {
|
||||
const show = !term || row.dataset.search.includes(term);
|
||||
row.style.display = show ? '' : 'none';
|
||||
const detail = document.getElementById('cdetail-' + row.dataset.cid);
|
||||
if (detail && !show) detail.style.display = 'none';
|
||||
});
|
||||
}
|
||||
|
||||
function toggleCustDetail(cid) {
|
||||
const row = document.getElementById('cdetail-' + cid);
|
||||
const btn = document.getElementById('cexpand-' + cid);
|
||||
const open = row.style.display !== 'table-row';
|
||||
row.style.display = open ? 'table-row' : 'none';
|
||||
btn.classList.toggle('open', open);
|
||||
}
|
||||
|
||||
function cvToggleReq(bid, field, btn) {
|
||||
btn.disabled = true;
|
||||
fetch('/admin/', {
|
||||
method:'POST',
|
||||
headers:{'Content-Type':'application/x-www-form-urlencoded','X-Requested-With':'XMLHttpRequest'},
|
||||
body:'action=toggle_requirement&id='+bid+'&field='+field+'&_t='+ADMIN_TOKEN
|
||||
}).then(r=>r.json()).then(d=>{
|
||||
if (!d.ok) { alert('Error saving'); btn.disabled=false; return; }
|
||||
const on = d.value === 1;
|
||||
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;
|
||||
const stepNums = {insurance_verified:'4', deposit_received:'5', license_verified:'6'};
|
||||
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'],
|
||||
};
|
||||
// Update customer-view elements
|
||||
const cvIcon = document.getElementById('cv-icon-'+bid+'-'+field);
|
||||
if (cvIcon) { cvIcon.className='flow-icon '+(on?'done':'pending'); cvIcon.textContent=on?'✓':stepNums[field]; }
|
||||
const cvMeta = document.getElementById('cv-meta-'+bid+'-'+field);
|
||||
if (cvMeta) cvMeta.textContent = metaTexts[field][on?1:0];
|
||||
// Mirror to main booking table
|
||||
const dot = document.getElementById('dot-'+bid+'-'+field);
|
||||
if (dot) dot.className = 'dot '+(on?'dot-done':'dot-pending');
|
||||
const icon = document.getElementById('icon-'+bid+'-'+field);
|
||||
if (icon) { icon.className='flow-icon '+(on?'done':'pending'); icon.textContent=on?'✓':stepNums[field]; }
|
||||
const meta = document.getElementById('meta-'+bid+'-'+field);
|
||||
if (meta) meta.textContent = metaTexts[field][on?1:0];
|
||||
const mainBtn = document.getElementById('btn-'+bid+'-'+field);
|
||||
if (mainBtn) { mainBtn.classList.toggle('active',on); mainBtn.textContent=on?labels[field][1]:labels[field][0]; }
|
||||
});
|
||||
}
|
||||
|
||||
function cvSquareAction(bid, action, btn) {
|
||||
const labels = {
|
||||
square_capture: ['Capture','Capturing…','Captured ✓'],
|
||||
square_void: ['Void Hold','Voiding…','Voided ✓'],
|
||||
square_refund: ['Refund','Refunding…','Refunded ✓'],
|
||||
};
|
||||
const [orig, working] = labels[action];
|
||||
const confirmMsg = {
|
||||
square_capture: 'Charge the $<?= number_format(DEPOSIT_AMOUNT,2) ?> deposit hold to this card?',
|
||||
square_void: 'Void the $<?= number_format(DEPOSIT_AMOUNT,2) ?> deposit hold? The customer will NOT be charged.',
|
||||
square_refund: 'Refund the deposit to this card?',
|
||||
}[action];
|
||||
if (!confirm(confirmMsg)) return;
|
||||
btn.disabled = true;
|
||||
btn.textContent = working;
|
||||
fetch('/admin/', {
|
||||
method:'POST',
|
||||
headers:{'Content-Type':'application/x-www-form-urlencoded','X-Requested-With':'XMLHttpRequest'},
|
||||
body:'action='+action+'&id='+bid+'&_t='+ADMIN_TOKEN
|
||||
}).then(r=>r.json()).then(d=>{
|
||||
if (d.ok) {
|
||||
const cvMeta = document.getElementById('cv-meta-'+bid+'-deposit_received');
|
||||
const cvIcon = document.getElementById('cv-icon-'+bid+'-deposit_received');
|
||||
const cvArea = document.getElementById('cv-dep-actions-'+bid);
|
||||
const meta = document.getElementById('meta-'+bid+'-deposit_received');
|
||||
const icon = document.getElementById('icon-'+bid+'-deposit_received');
|
||||
const dot = document.getElementById('dot-'+bid+'-deposit_received');
|
||||
const area = document.getElementById('deposit-actions-'+bid);
|
||||
if (d.status === 'COMPLETED') {
|
||||
const t = 'Captured — deposit charged';
|
||||
if (cvMeta) cvMeta.textContent = t;
|
||||
if (cvIcon) { cvIcon.className='flow-icon done'; cvIcon.textContent='✓'; }
|
||||
if (cvArea) cvArea.innerHTML = '<button class="flow-toggle" onclick="cvSquareAction('+bid+',\'square_refund\',this)" style="border-color:#2563eb;color:#2563eb">Refund Deposit</button>';
|
||||
if (meta) meta.textContent = t;
|
||||
if (icon) { icon.className='flow-icon done'; icon.textContent='✓'; }
|
||||
if (dot) dot.className='dot dot-done';
|
||||
if (area) area.innerHTML = '<button class="flow-toggle" onclick="squareAction('+bid+',\'square_refund\',this)" style="border-color:#2563eb;color:#2563eb">Refund Deposit</button>';
|
||||
} else if (d.status === 'CANCELED') {
|
||||
const t = 'Hold voided — no charge';
|
||||
if (cvMeta) cvMeta.textContent = t;
|
||||
if (cvIcon) { cvIcon.className='flow-icon skip'; cvIcon.textContent='—'; }
|
||||
if (cvArea) cvArea.innerHTML = '';
|
||||
if (meta) meta.textContent = t;
|
||||
if (icon) { icon.className='flow-icon skip'; icon.textContent='—'; }
|
||||
if (dot) dot.className='dot dot-skip';
|
||||
if (area) area.innerHTML = '';
|
||||
} else if (d.status === 'REFUNDED') {
|
||||
const t = 'Refunded — deposit returned';
|
||||
if (cvMeta) cvMeta.textContent = t;
|
||||
if (cvIcon) { cvIcon.className='flow-icon pending'; cvIcon.textContent='↩'; }
|
||||
if (cvArea) cvArea.innerHTML = '';
|
||||
if (meta) meta.textContent = t;
|
||||
if (icon) { icon.className='flow-icon pending'; icon.textContent='↩'; }
|
||||
if (dot) dot.className='dot dot-skip';
|
||||
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.'); });
|
||||
}
|
||||
|
||||
function cvSaveNotes(bid) {
|
||||
const ta = document.getElementById('cv-notes-' + bid);
|
||||
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='+bid+'¬es='+encodeURIComponent(ta.value)+'&_t='+ADMIN_TOKEN
|
||||
}).then(r=>r.json()).then(d=>{
|
||||
btn.textContent = d.ok ? 'Saved ✓' : 'Error';
|
||||
setTimeout(()=>btn.textContent='Save Notes', 1800);
|
||||
// Mirror to main booking table notes
|
||||
const mainTa = document.getElementById('notes-'+bid);
|
||||
if (mainTa) mainTa.value = ta.value;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1219,13 +1577,30 @@ function showCustMsg(text, color) {
|
||||
// ── Block / unblock dates ─────────────────────────────────────────────────────
|
||||
function blockDate(e) {
|
||||
e.preventDefault();
|
||||
var date = document.getElementById('blockDate').value;
|
||||
var reason = document.getElementById('blockReason').value;
|
||||
var dateVal = document.getElementById('blockDate').value;
|
||||
var reason = document.getElementById('blockReason').value;
|
||||
fetch('/admin/', {
|
||||
method:'POST',
|
||||
headers:{'Content-Type':'application/x-www-form-urlencoded','X-Requested-With':'XMLHttpRequest'},
|
||||
body:'action=block_date&date='+date+'&reason='+encodeURIComponent(reason)+'&_t='+ADMIN_TOKEN
|
||||
}).then(r=>r.json()).then(d=>{ if(d.ok) location.reload(); else alert(d.error); });
|
||||
body:'action=block_date&date='+dateVal+'&reason='+encodeURIComponent(reason)+'&_t='+ADMIN_TOKEN
|
||||
}).then(r=>r.json()).then(d=>{
|
||||
if (d.ok) {
|
||||
var list = document.querySelector('.block-list');
|
||||
var empty = list.querySelector('p');
|
||||
if (empty) empty.remove();
|
||||
var dt = new Date(dateVal + 'T12:00:00');
|
||||
var label = dt.toLocaleDateString('en-US',{month:'short',day:'numeric',year:'numeric'});
|
||||
var item = document.createElement('div');
|
||||
item.className = 'block-item';
|
||||
item.id = 'blocked-' + d.id;
|
||||
item.innerHTML = '<button class="del-btn" onclick="unblockDate('+d.id+')" title="Remove">✕</button>'
|
||||
+ '<strong>' + label + '</strong>'
|
||||
+ (d.reason ? '<span style="color:#6b7280"> — ' + d.reason.replace(/</g,'<') + '</span>' : '');
|
||||
list.appendChild(item);
|
||||
document.getElementById('blockDate').value = '';
|
||||
document.getElementById('blockReason').value = '';
|
||||
} else { alert(d.error); }
|
||||
});
|
||||
}
|
||||
function unblockDate(id) {
|
||||
if (!confirm('Remove this blocked date?')) return;
|
||||
|
||||
Reference in New Issue
Block a user