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 @@
+
+
+
+
|