diff --git a/admin/index.php b/admin/index.php index 23a0a40..50ff63d 100644 --- a/admin/index.php +++ b/admin/index.php @@ -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} @@ -887,31 +904,241 @@ textarea.notes-ta:focus{border-color:#f97316} + - - + - - + + + - + - - + - + + + + + @@ -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 $ deposit hold to this card?', + square_void: 'Void the $ 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 = ''; + 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 = ''; + } 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 = '' + + '' + label + '' + + (d.reason ? ' — ' + d.reason.replace(/' : ''); + 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;
Name Email PhoneDOB Bookings Status Added
+ + - + onclick="deleteCustomer(,this)">Del +