Customer detail: expandable rows with full booking flow + payment controls

This commit is contained in:
2026-05-22 22:06:49 +00:00
parent 3b0daa72e4
commit 41773a6905
+395 -20
View File
@@ -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 ?>)">&#9658;</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']) ?> &middot; <?= date('M j, Y', strtotime($cb['rental_date'])) ?> &middot; $<?= 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 &amp; Balance — $<?= number_format(DEPOSIT_AMOUNT,2) ?> held &middot; $<?= 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+'&notes='+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,'&lt;') + '</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;