feat: two-step bar charge flow with full-screen PIN overlay

Staff enter amount and note, click Charge; a full-screen patron-facing
screen appears showing the charge total, member name, and a large PIN
input that auto-focuses. PIN errors stay on the overlay; Cancel returns
to the amount form so staff can adjust before handing the device back.

https://claude.ai/code/session_01JuRTR5Xjx8emQsyerBgGU7
This commit is contained in:
Claude 2026-05-30 17:38:18 +00:00
parent a5b4ed8389
commit 1f8fe21eaf
No known key found for this signature in database
3 changed files with 128 additions and 11 deletions

View file

@ -370,18 +370,44 @@ function clearBarSelection() {
barMember = null;
document.getElementById('barForm').classList.add('hidden');
document.getElementById('barAmount').value = '';
document.getElementById('barPin').value = '';
document.getElementById('barNote').value = '';
document.getElementById('barNote').value = '';
hidePinOverlay();
setMsg('barMsg', '', '');
}
async function doCharge() {
function hidePinOverlay() {
document.getElementById('pinOverlay').classList.add('hidden');
document.getElementById('barPin').value = '';
setMsg('pinMsg', '', '');
}
function cancelPin() {
hidePinOverlay();
// return focus to amount so staff can adjust
document.getElementById('barAmount').focus();
}
function prepareCharge() {
if (!barMember) return;
const amount = toMinor('barAmount');
if (!amount) { setMsg('barMsg', 'Enter a valid amount.', 'err'); return; }
setMsg('barMsg', '', '');
// populate overlay
document.getElementById('pinAmount').textContent =
'Charge: ' + fmtAmount(amount);
document.getElementById('pinMember').textContent = barMember.name;
document.getElementById('barPin').value = '';
setMsg('pinMsg', '', '');
document.getElementById('pinOverlay').classList.remove('hidden');
document.getElementById('barPin').focus();
}
async function confirmCharge() {
if (!barMember) return;
const amount = toMinor('barAmount');
const pin = document.getElementById('barPin').value;
const note = document.getElementById('barNote').value.trim();
if (!amount) { setMsg('barMsg', 'Enter a valid amount.', 'err'); return; }
if (!pin) { setMsg('barMsg', 'PIN required.', 'err'); return; }
if (!pin) { setMsg('pinMsg', 'PIN required.', 'err'); return; }
try {
const r = await apiFetch('/charge', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
@ -390,7 +416,7 @@ async function doCharge() {
window.open(`/receipt/${r.entry_id}`, '_blank');
setMsg('barMsg', `Charge complete. New balance: ${r.new_balance_display}`, 'ok');
clearBarSelection();
} catch (err) { setMsg('barMsg', err.message, 'err'); }
} catch (err) { setMsg('pinMsg', err.message, 'err'); }
}
// ---------------------------------------------------------------------------

View file

@ -165,21 +165,34 @@
<label>Amount (<span class="currency-unit"></span>)</label>
<input type="number" id="barAmount" placeholder="e.g. 3.50" min="0.01" step="0.01">
</div>
<div class="form-row">
<label>Member PIN</label>
<input type="password" id="barPin" placeholder="Member PIN" maxlength="20">
</div>
<div class="form-row">
<label>Note (optional)</label>
<input type="text" id="barNote" placeholder="">
</div>
<button class="btn btn-danger" onclick="doCharge()">Charge</button>
<button class="btn btn-primary" onclick="prepareCharge()">Charge</button>
<button class="btn" onclick="clearBarSelection()">Cancel</button>
</div>
<div id="barMsg" class="msg"></div>
</div>
</div>
<!-- PIN confirmation overlay (patron-facing) -->
<div id="pinOverlay" class="pin-overlay hidden">
<div class="pin-card">
<div class="pin-charge-amount" id="pinAmount"></div>
<div class="pin-member-name" id="pinMember"></div>
<div class="pin-instruction">Please enter your PIN</div>
<input type="password" id="barPin" class="pin-input" placeholder="••••"
maxlength="20" autocomplete="off" inputmode="numeric"
onkeydown="if(event.key==='Enter') confirmCharge()">
<div id="pinMsg" class="msg"></div>
<div class="pin-actions">
<button class="btn pin-cancel-btn" onclick="cancelPin()">Cancel</button>
<button class="btn btn-primary pin-ok-btn" onclick="confirmCharge()">OK</button>
</div>
</div>
</div>
<!-- ===================== ADMIN VIEW ===================== -->
<div id="view-admin" class="view hidden">

View file

@ -409,3 +409,81 @@ select {
.form-row-check label { font-weight: 400; color: var(--fg); font-size: .875rem; }
.panel-divider { border: none; border-top: 1px solid var(--border); margin: 18px 0; }
.sub-heading { font-size: .875rem; font-weight: 600; margin-bottom: 12px; color: var(--fg); }
/* ---- PIN confirmation overlay ---- */
.pin-overlay {
position: fixed;
inset: 0;
background: var(--canvas);
display: flex;
align-items: center;
justify-content: center;
z-index: 150;
padding: 24px 16px;
}
.pin-card {
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
width: 100%;
max-width: 380px;
}
.pin-charge-amount {
font-size: clamp(2rem, 8vw, 3rem);
font-weight: 700;
color: var(--fg);
text-align: center;
line-height: 1.15;
}
.pin-member-name {
font-size: 1.0625rem;
color: var(--fg-muted);
text-align: center;
}
.pin-instruction {
font-size: .875rem;
color: var(--fg-subtle);
text-align: center;
margin-top: 8px;
}
.pin-input {
width: 100%;
max-width: 260px;
padding: 14px 16px;
font-size: 1.5rem;
letter-spacing: .25em;
text-align: center;
border: 2px solid var(--border);
border-radius: var(--radius);
outline: none;
background: var(--canvas);
color: var(--fg);
transition: border-color .15s, box-shadow .15s;
}
.pin-input:focus {
border-color: var(--accent);
box-shadow: 0 0 0 3px rgba(9,105,218,.15);
}
.pin-actions {
display: flex;
gap: 12px;
width: 100%;
max-width: 260px;
}
.pin-cancel-btn {
flex: 1;
}
.pin-ok-btn {
flex: 2;
font-size: 1rem;
padding: 10px 0;
}