Swap steps 5/6 (license before deposit), charge balance guard, refund cancels booking

This commit is contained in:
2026-05-25 18:49:35 +00:00
parent 3e18d71378
commit af5712f761
+111 -86
View File
@@ -198,7 +198,7 @@ if ($isAjax) {
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]);
db()->prepare("UPDATE bookings SET square_payment_status='CANCELED', status='cancelled' WHERE id=?")->execute([$id]);
echo json_encode(['ok'=>true,'status'=>'CANCELED']);
} else {
echo json_encode(['error' => $resp['errors'][0]['detail'] ?? 'Void failed']);
@@ -221,7 +221,7 @@ if ($isAjax) {
'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=?")
db()->prepare("UPDATE bookings SET square_payment_status='REFUNDED', square_refund_id=?, deposit_paid=0, status='cancelled' WHERE id=?")
->execute([$resp['refund']['id'], $id]);
echo json_encode(['ok'=>true,'status'=>'REFUNDED']);
} else {
@@ -686,12 +686,12 @@ textarea.notes-ta:focus{border-color:#f97316}
</select>
</td>
<td>
<div class="dots" title="Confirmed / Waiver / Insurance / Deposit / License / Helmet / Safety / Ops / Returned">
<div class="dots" title="Confirmed / Waiver / Insurance / License / Deposit / Helmet / Safety / Ops / Returned">
<div class="dot <?= $dotClass($stepConfirmed) ?>" title="Confirmed"></div>
<div class="dot <?= $dotClass($stepWaiver) ?>" title="Waiver Signed"></div>
<div class="dot <?= $dotClass($stepInsurance) ?>" title="Insurance" id="dot-<?= $bid ?>-insurance_verified"></div>
<div class="dot <?= $dotClass($stepDeposit) ?>" title="Deposit" id="dot-<?= $bid ?>-deposit_received"></div>
<div class="dot <?= $dotClass($stepLicense) ?>" title="License" id="dot-<?= $bid ?>-license_verified"></div>
<div class="dot <?= $dotClass($stepDeposit) ?>" title="Deposit" id="dot-<?= $bid ?>-deposit_received"></div>
<div class="dot <?= $dotClass($stepHelmet) ?>" title="Helmet" id="dot-<?= $bid ?>-helmet_provided"></div>
<div class="dot <?= $dotClass($stepSafety) ?>" title="Safety Course" id="dot-<?= $bid ?>-safety_course"></div>
<div class="dot <?= $dotClass($stepOps) ?>" title="Ops Course" id="dot-<?= $bid ?>-operational_course"></div>
@@ -812,7 +812,29 @@ textarea.notes-ta:focus{border-color:#f97316}
</div>
</div>
<!-- Step 5: Deposit (Square-aware) -->
<!-- Step 5: License -->
<div class="flow-step">
<div class="flow-icon <?= $stepLicense?'done':($cancelled?'skip':'pending') ?>" id="icon-<?= $bid ?>-license_verified">
<?= $stepLicense?'✓':($cancelled?'—':'5') ?>
</div>
<div class="flow-body">
<span class="flow-label">Driver's License Verified</span>
<span class="flow-meta" id="meta-<?= $bid ?>-license_verified">
<?= $stepLicense?'License verified':($cancelled?'N/A':'Verify at pickup') ?>
</span>
<?php if (!$cancelled): ?>
<div class="flow-action">
<button class="flow-toggle <?= $stepLicense?'active':'' ?>"
id="btn-<?= $bid ?>-license_verified"
onclick="toggleReq(<?= $bid ?>,'license_verified',this)">
<?= $stepLicense?'✓ License Verified':'Mark License Verified' ?>
</button>
</div>
<?php endif; ?>
</div>
</div>
<!-- Step 6: Deposit (Square-aware) -->
<?php
$sqStatus = $b['square_payment_status'] ?? '';
$sqId = $b['square_payment_id'] ?? '';
@@ -820,10 +842,11 @@ textarea.notes-ta:focus{border-color:#f97316}
$cardLast4 = $b['card_last4'] ?? '';
$cardBrand = $b['card_brand'] ?? '';
$depositIcon = 'pending';
$depositLabel = '5';
$depositLabel = '6';
if ($cancelled) { $depositIcon='skip'; $depositLabel='—'; }
elseif ($stepDeposit ||
in_array($sqStatus,['COMPLETED','BAL_COMPLETED'])) { $depositIcon='done'; $depositLabel='✓'; }
$priorStepsDone = $stepConfirmed && $stepWaiver && $stepInsurance && $stepLicense;
?>
<div class="flow-step">
<div class="flow-icon <?= $depositIcon ?>" id="icon-<?= $bid ?>-deposit_received">
@@ -853,14 +876,18 @@ textarea.notes-ta:focus{border-color:#f97316}
<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 if ($sqCardId): ?>
<button class="flow-toggle" onclick="squareAction(<?= $bid ?>,'square_void',this)" style="border-color:#dc2626;color:#dc2626">Void &amp; Cancel Booking</button>
<?php if ($sqCardId && $priorStepsDone): ?>
<button class="flow-toggle" onclick="chargeBalance(<?= $bid ?>,this)" style="margin-left:4px;border-color:#16a34a;color:#16a34a">Charge Balance $<?= number_format($b['amount']-DEPOSIT_AMOUNT,2) ?></button>
<?php elseif ($sqCardId): ?>
<button class="flow-toggle" disabled style="margin-left:4px;border-color:#9ca3af;color:#9ca3af;opacity:.55;cursor:not-allowed" title="Complete steps 15 before charging balance">Charge Balance $<?= number_format($b['amount']-DEPOSIT_AMOUNT,2) ?></button>
<?php endif; ?>
<?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 if ($sqCardId): ?>
<button class="flow-toggle" onclick="squareAction(<?= $bid ?>,'square_refund',this)" style="border-color:#2563eb;color:#2563eb">Refund &amp; Cancel Booking</button>
<?php if ($sqCardId && $priorStepsDone): ?>
<button class="flow-toggle" onclick="chargeBalance(<?= $bid ?>,this)" style="margin-left:4px;border-color:#16a34a;color:#16a34a">Charge Balance $<?= number_format($b['amount']-DEPOSIT_AMOUNT,2) ?></button>
<?php elseif ($sqCardId): ?>
<button class="flow-toggle" disabled style="margin-left:4px;border-color:#9ca3af;color:#9ca3af;opacity:.55;cursor:not-allowed" title="Complete steps 15 before charging balance">Charge Balance $<?= number_format($b['amount']-DEPOSIT_AMOUNT,2) ?></button>
<?php endif; ?>
<?php elseif (!$sqId || $sqStatus === 'DECLINED'): ?>
<button class="flow-toggle <?= $stepDeposit?'active':'' ?>"
@@ -874,28 +901,6 @@ textarea.notes-ta:focus{border-color:#f97316}
</div>
</div>
<!-- Step 6: License -->
<div class="flow-step">
<div class="flow-icon <?= $stepLicense?'done':($cancelled?'skip':'pending') ?>" id="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="meta-<?= $bid ?>-license_verified">
<?= $stepLicense?'License verified':($cancelled?'N/A':'Verify at pickup') ?>
</span>
<?php if (!$cancelled): ?>
<div class="flow-action">
<button class="flow-toggle <?= $stepLicense?'active':'' ?>"
id="btn-<?= $bid ?>-license_verified"
onclick="toggleReq(<?= $bid ?>,'license_verified',this)">
<?= $stepLicense?'✓ License Verified':'Mark License Verified' ?>
</button>
</div>
<?php endif; ?>
</div>
</div>
<?php if (!$cancelled): ?>
<!-- ── Pre-departure checklist (admin only, after deposit confirmed) ── -->
<div style="margin:.6rem 0 .2rem;padding:.4rem .5rem;background:#fff7ed;border-radius:6px;font-size:.7rem;font-weight:700;text-transform:uppercase;letter-spacing:.5px;color:#92400e">
@@ -1010,14 +1015,14 @@ textarea.notes-ta:focus{border-color:#f97316}
<input type="checkbox" value="insurance" <?= !$stepInsurance?'checked':'' ?>>
Bring proof of insurance
</label>
<label>
<input type="checkbox" value="deposit" <?= !$stepDeposit?'checked':'' ?>>
Security deposit info
</label>
<label>
<input type="checkbox" value="license" <?= !$stepLicense?'checked':'' ?>>
Bring driver's license
</label>
<label>
<input type="checkbox" value="deposit" <?= !$stepDeposit?'checked':'' ?>>
Security deposit info
</label>
</div>
<button class="send-btn" id="remind-btn-<?= $bid ?>" onclick="sendReminder(<?= $bid ?>)">
Send Reminder Email
@@ -1292,9 +1297,29 @@ textarea.notes-ta:focus{border-color:#f97316}
</div>
</div>
<div class="flow-step">
<div class="flow-icon <?= $stepLicense?'done':($cancelled?'skip':'pending') ?>" id="cv-icon-<?= $bid ?>-license_verified">
<?= $stepLicense?'✓':($cancelled?'—':'5') ?>
</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>
<?php
$cvDepIcon = $cancelled ? 'skip' : (($stepDeposit||$sqStatus==='COMPLETED') ? 'done' : 'pending');
$cvDepLabel = $cancelled ? '—' : (($stepDeposit||$sqStatus==='COMPLETED') ? '✓' : '5');
$cvDepLabel = $cancelled ? '—' : (($stepDeposit||$sqStatus==='COMPLETED') ? '✓' : '6');
?>
<div class="flow-step">
<div class="flow-icon <?= $cvDepIcon ?>" id="cv-icon-<?= $bid ?>-deposit_received">
@@ -1316,9 +1341,9 @@ textarea.notes-ta:focus{border-color:#f97316}
<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>
<button class="flow-toggle" onclick="cvSquareAction(<?= $bid ?>,'square_void',this)" style="border-color:#dc2626;color:#dc2626">Void &amp; Cancel Booking</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>
<button class="flow-toggle" onclick="cvSquareAction(<?= $bid ?>,'square_refund',this)" style="border-color:#2563eb;color:#2563eb">Refund &amp; Cancel Booking</button>
<?php elseif (!$sqId): ?>
<button class="flow-toggle <?= $stepDeposit?'active':'' ?>" id="cv-btn-<?= $bid ?>-deposit_received"
onclick="cvToggleReq(<?= $bid ?>,'deposit_received',this)">
@@ -1330,26 +1355,6 @@ textarea.notes-ta:focus{border-color:#f97316}
</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>
<?php if (!$cancelled): ?>
<div style="margin:.4rem 0 .1rem;padding:.3rem .5rem;background:#fff7ed;border-radius:5px;font-size:.68rem;font-weight:700;text-transform:uppercase;letter-spacing:.5px;color:#92400e">Pre-Departure</div>
@@ -1532,7 +1537,7 @@ function toggleReq(id, field, btn) {
const icon = document.getElementById('icon-'+id+'-'+field);
if (icon) {
icon.className = 'flow-icon ' + (on ? 'done' : 'pending');
const stepNums = {insurance_verified:'4', deposit_received:'5', license_verified:'6', helmet_provided:'7', safety_course:'8', operational_course:'9'};
const stepNums = {insurance_verified:'4', license_verified:'5', deposit_received:'6', helmet_provided:'7', safety_course:'8', operational_course:'9'};
icon.textContent = on ? '✓' : (stepNums[field] || '?');
}
// Update meta text
@@ -1624,8 +1629,8 @@ function squareAction(id, action, btn) {
const [orig, working, done] = 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?',
square_void: 'Void the $<?= number_format(DEPOSIT_AMOUNT,2) ?> deposit hold and CANCEL this booking? The customer will NOT be charged.',
square_refund: 'Refund the $<?= number_format(DEPOSIT_AMOUNT,2) ?> deposit and CANCEL this booking? This cannot be undone.',
}[action];
if (!confirm(confirmMsg)) return;
btn.disabled = true;
@@ -1650,19 +1655,31 @@ function squareAction(id, action, btn) {
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 (area) area.innerHTML = '<button class="flow-toggle" onclick="squareAction('+id+',\'square_refund\',this)" style="border-color:#2563eb;color:#2563eb">Refund &amp; Cancel Booking</button>';
} else if (d.status === 'CANCELED' || d.status === 'REFUNDED') {
const t = d.status === 'CANCELED' ? 'Hold voided — booking cancelled' : 'Refunded — booking cancelled';
if (meta) meta.textContent = t;
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 = '';
// Update booking status selects and row data-status
document.querySelectorAll('select.status-sel[data-id="'+id+'"]').forEach(sel => {
sel.value = 'cancelled';
});
document.querySelectorAll('tr.booking-row, tr.detail-row').forEach(row => {
const s = row.querySelector('select[data-id="'+id+'"]');
if (s) row.dataset.status = 'cancelled';
});
// Update confirmed step icon
['icon','cv-icon'].forEach(p => {
const el = document.getElementById(p+'-'+id+'-confirmed');
if (el) { el.className='flow-icon skip'; el.textContent='—'; }
});
['meta','cv-meta'].forEach(p => {
const el = document.getElementById(p+'-'+id+'-confirmed');
if (el) el.textContent = 'Cancelled';
});
}
} else {
btn.textContent = orig;
@@ -1830,8 +1847,8 @@ function cvSquareAction(bid, action, btn) {
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?',
square_void: 'Void the $<?= number_format(DEPOSIT_AMOUNT,2) ?> deposit hold and CANCEL this booking? The customer will NOT be charged.',
square_refund: 'Refund the $<?= number_format(DEPOSIT_AMOUNT,2) ?> deposit and CANCEL this booking? This cannot be undone.',
}[action];
if (!confirm(confirmMsg)) return;
btn.disabled = true;
@@ -1853,13 +1870,13 @@ function cvSquareAction(bid, action, btn) {
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 (cvArea) cvArea.innerHTML = '<button class="flow-toggle" onclick="cvSquareAction('+bid+',\'square_refund\',this)" style="border-color:#2563eb;color:#2563eb">Refund &amp; Cancel Booking</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 (area) area.innerHTML = '<button class="flow-toggle" onclick="squareAction('+bid+',\'square_refund\',this)" style="border-color:#2563eb;color:#2563eb">Refund &amp; Cancel Booking</button>';
} else if (d.status === 'CANCELED' || d.status === 'REFUNDED') {
const t = d.status === 'CANCELED' ? 'Hold voided — booking cancelled' : 'Refunded — booking cancelled';
if (cvMeta) cvMeta.textContent = t;
if (cvIcon) { cvIcon.className='flow-icon skip'; cvIcon.textContent='—'; }
if (cvArea) cvArea.innerHTML = '';
@@ -1867,15 +1884,23 @@ function cvSquareAction(bid, action, btn) {
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 = '';
// Update booking status selects and row data-status
document.querySelectorAll('select.status-sel[data-id="'+bid+'"]').forEach(sel => {
sel.value = 'cancelled';
});
document.querySelectorAll('tr.booking-row, tr.detail-row').forEach(row => {
const s = row.querySelector('select[data-id="'+bid+'"]');
if (s) row.dataset.status = 'cancelled';
});
// Update confirmed step icon
['icon','cv-icon'].forEach(p => {
const el = document.getElementById(p+'-'+bid+'-confirmed');
if (el) { el.className='flow-icon skip'; el.textContent='—'; }
});
['meta','cv-meta'].forEach(p => {
const el = document.getElementById(p+'-'+bid+'-confirmed');
if (el) el.textContent = 'Cancelled';
});
}
} else {
btn.textContent = orig;