mirror of
https://github.com/kbenestad/ClubLedger.git
synced 2026-06-18 09:44:33 +00:00
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:
parent
a5b4ed8389
commit
1f8fe21eaf
3 changed files with 128 additions and 11 deletions
|
|
@ -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'); }
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue