diff --git a/main.py b/main.py index 58e4a01..183facb 100644 --- a/main.py +++ b/main.py @@ -71,10 +71,11 @@ def init_db(): with db_conn() as conn: conn.executescript(""" CREATE TABLE IF NOT EXISTS members ( - id INTEGER PRIMARY KEY AUTOINCREMENT, + id INTEGER PRIMARY KEY AUTOINCREMENT, member_number TEXT UNIQUE NOT NULL, name TEXT NOT NULL, pin_hash TEXT NOT NULL, + overdraft_override INTEGER DEFAULT NULL, created_at TEXT NOT NULL DEFAULT (datetime('now')) ); CREATE TABLE IF NOT EXISTS ledger_entries ( @@ -117,6 +118,11 @@ def init_db(): def migrate_db(): """Run schema migrations that can't be expressed as CREATE TABLE IF NOT EXISTS.""" with db_conn() as conn: + # --- members: add overdraft_override column --- + cols = [r[1] for r in conn.execute("PRAGMA table_info(members)").fetchall()] + if "overdraft_override" not in cols: + conn.execute("ALTER TABLE members ADD COLUMN overdraft_override INTEGER DEFAULT NULL") + # --- staff_accounts: add cashier/pos-staff roles --- schema = conn.execute( "SELECT sql FROM sqlite_master WHERE type='table' AND name='staff_accounts'" @@ -299,9 +305,10 @@ class MemberCreate(BaseModel): return v class MemberUpdate(BaseModel): - member_number: Optional[str] = None - name: Optional[str] = None - pin: Optional[str] = None + member_number: Optional[str] = None + name: Optional[str] = None + pin: Optional[str] = None + overdraft_override: Optional[int] = None # NULL=default, 1=allow, 0=block class TopupRequest(BaseModel): member_id: int @@ -309,11 +316,10 @@ class TopupRequest(BaseModel): note: Optional[str] = None class ChargeRequest(BaseModel): - member_id: int - amount: int - pin: str - note: Optional[str] = None - overdraft_override: bool = False + member_id: int + amount: int + pin: str + note: Optional[str] = None class WithdrawalRequest(BaseModel): member_id: int @@ -450,6 +456,8 @@ def update_member(member_id: int, body: MemberUpdate, user: dict = Depends(curre if body.pin is not None: if len(body.pin) < 4: raise HTTPException(400, "PIN must be at least 4 characters") updates["pin_hash"] = hash_pin(body.pin) + if "overdraft_override" in body.model_fields_set: + updates["overdraft_override"] = body.overdraft_override # None, 0, or 1 if updates: conn.execute( f"UPDATE members SET {', '.join(f'{k}=?' for k in updates)} WHERE id=?", @@ -486,6 +494,7 @@ def list_members(q: Optional[str] = None, user: dict = Depends(current_user)): bal = member_balance(conn, r["id"]) result.append({ "id": r["id"], "member_number": r["member_number"], "name": r["name"], + "overdraft_override": r["overdraft_override"], "balance": bal, "balance_display": format_amount(bal), "created_at": r["created_at"], }) return result @@ -523,15 +532,16 @@ def charge(body: ChargeRequest, user: dict = Depends(pos_user)): raise HTTPException(403, "Incorrect PIN") bal = member_balance(conn, body.member_id) policy = s.get("overdraft_policy", "never") - if policy == "always": + member_ov = member["overdraft_override"] # None, 0, or 1 + if policy == "never": + overdraft_ok = False + elif policy == "always": overdraft_ok = True - elif policy == "staff_override": - overdraft_ok = body.overdraft_override - elif policy == "admin_override": - overdraft_ok = body.overdraft_override and user["role"] == "admin" + elif policy in ("staff_override", "admin_override"): + overdraft_ok = (member_ov == 1) elif policy == "staff_block": - overdraft_ok = not body.overdraft_override - else: # "never" or unknown + overdraft_ok = (member_ov != 0) # None or 1 = allowed; 0 = explicitly blocked + else: overdraft_ok = False if not overdraft_ok and bal < body.amount: raise HTTPException(400, f"Insufficient balance ({format_amount(bal)})") diff --git a/static/app.js b/static/app.js index f938099..51f1c74 100644 --- a/static/app.js +++ b/static/app.js @@ -160,7 +160,7 @@ function renderMemberTable(members) { ${m.created_at ? m.created_at.slice(0, 10) : ''} Statement - + ${m.balance === 0 ? `` : ''} @@ -169,12 +169,30 @@ function renderMemberTable(members) { } // Edit member modal -function openEditModal(id, name, number) { +function openEditModal(id, name, number, overdraftOverride) { editMemberId = id; document.getElementById('edit-number').value = number; document.getElementById('edit-name').value = name; document.getElementById('edit-pin').value = ''; setMsg('editMsg', '', ''); + + const policy = cfg.overdraft_policy || 'never'; + const overrideRow = document.getElementById('editOverdraftRow'); + const overrideCheck = document.getElementById('edit-overdraft'); + const overrideLabel = document.getElementById('editOverdraftLabel'); + + if (policy === 'staff_override' || (policy === 'admin_override' && currentUser.role === 'admin')) { + overrideLabel.textContent = 'Allow overdraft for this member'; + overrideCheck.checked = (overdraftOverride === 1); + overrideRow.classList.remove('hidden'); + } else if (policy === 'staff_block') { + overrideLabel.textContent = 'Block overdraft for this member'; + overrideCheck.checked = (overdraftOverride === 0); + overrideRow.classList.remove('hidden'); + } else { + overrideRow.classList.add('hidden'); + } + document.getElementById('editModal').classList.remove('hidden'); document.getElementById('edit-name').focus(); } @@ -192,6 +210,17 @@ async function saveEdit() { }; const pin = document.getElementById('edit-pin').value; if (pin) body.pin = pin; + + const policy = cfg.overdraft_policy || 'never'; + const overrideRow = document.getElementById('editOverdraftRow'); + if (!overrideRow.classList.contains('hidden')) { + const checked = document.getElementById('edit-overdraft').checked; + if (policy === 'staff_block') { + body.overdraft_override = checked ? 0 : null; + } else { + body.overdraft_override = checked ? 1 : null; + } + } try { await apiFetch(`/members/${editMemberId}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, @@ -308,35 +337,14 @@ function selectBarMember(id, name, number, balance, balanceDisplay) { `${esc(name)}   #${esc(number)}   Balance: ${esc(balanceDisplay)}`; document.getElementById('barForm').classList.remove('hidden'); setMsg('barMsg', '', ''); - - const policy = cfg.overdraft_policy || 'never'; - const overrideRow = document.getElementById('barOverrideRow'); - const overrideCheck = document.getElementById('barOverrideCheck'); - const overrideLabel = document.getElementById('barOverrideLabel'); - overrideCheck.checked = false; - - if (policy === 'staff_override') { - overrideLabel.textContent = 'Allow overdraft for this transaction'; - overrideRow.classList.remove('hidden'); - } else if (policy === 'admin_override' && currentUser.role === 'admin') { - overrideLabel.textContent = 'Allow overdraft for this transaction'; - overrideRow.classList.remove('hidden'); - } else if (policy === 'staff_block') { - overrideLabel.textContent = 'Block if insufficient balance'; - overrideRow.classList.remove('hidden'); - } else { - overrideRow.classList.add('hidden'); - } } function clearBarSelection() { barMember = null; document.getElementById('barForm').classList.add('hidden'); - document.getElementById('barAmount').value = ''; - document.getElementById('barPin').value = ''; - document.getElementById('barNote').value = ''; - document.getElementById('barOverrideCheck').checked = false; - document.getElementById('barOverrideRow').classList.add('hidden'); + document.getElementById('barAmount').value = ''; + document.getElementById('barPin').value = ''; + document.getElementById('barNote').value = ''; setMsg('barMsg', '', ''); } @@ -347,11 +355,10 @@ async function doCharge() { 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; } - const overdraft_override = document.getElementById('barOverrideCheck').checked; try { const r = await apiFetch('/charge', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ member_id: barMember.id, amount, pin, note: note || null, overdraft_override }) + body: JSON.stringify({ member_id: barMember.id, amount, pin, note: note || null }) }); window.open(`/receipt/${r.entry_id}`, '_blank'); setMsg('barMsg', `Charge complete. New balance: ${r.new_balance_display}`, 'ok'); diff --git a/static/index.html b/static/index.html index e6223eb..b2d786b 100644 --- a/static/index.html +++ b/static/index.html @@ -153,10 +153,6 @@ - @@ -236,6 +232,10 @@ +