feat: add member details popup with attendance and fee exceptions
All checks were successful
Deploy to K8s / deploy (push) Successful in 12s
Build and Push / build (push) Successful in 8s

This commit is contained in:
Jan Novak
2026-03-02 21:41:36 +01:00
parent 99b23199b1
commit 815b962dd7
7 changed files with 512 additions and 16 deletions

View File

@@ -233,10 +233,49 @@ def fetch_sheet_data(spreadsheet_id: str, credentials_path: str) -> list[dict]:
return transactions
def fetch_exceptions(spreadsheet_id: str, credentials_path: str) -> dict[tuple[str, str], dict]:
"""Fetch manual fee overrides from the 'exceptions' sheet.
Returns a dict mapping (member_name, period_YYYYMM) to {'amount': int, 'note': str}.
"""
service = get_sheets_service(credentials_path)
try:
result = service.spreadsheets().values().get(
spreadsheetId=spreadsheet_id,
range="'exceptions'!A2:D",
valueRenderOption="UNFORMATTED_VALUE"
).execute()
rows = result.get("values", [])
except Exception as e:
print(f"Warning: Could not fetch exceptions: {e}")
return {}
exceptions = {}
for row in rows:
if len(row) < 3 or str(row[0]).lower().startswith("name"):
continue
name = str(row[0]).strip()
period = str(row[1]).strip()
# Robust normalization using czech_utils.normalize
norm_name = normalize(name)
norm_period = normalize(period)
try:
amount = int(row[2])
note = str(row[3]).strip() if len(row) > 3 else ""
exceptions[(norm_name, norm_period)] = {"amount": amount, "note": note}
except (ValueError, TypeError):
continue
return exceptions
def reconcile(
members: list[tuple[str, str, dict[str, int]]],
sorted_months: list[str],
transactions: list[dict],
exceptions: dict[tuple[str, str], dict] = None,
) -> dict:
"""Match transactions to members and months.
@@ -251,11 +290,30 @@ def reconcile(
# Initialize ledger
ledger: dict[str, dict[str, dict]] = {}
exceptions = exceptions or {}
for name in member_names:
ledger[name] = {}
for m in sorted_months:
# Robust normalization for lookup
norm_name = normalize(name)
norm_period = normalize(m)
fee_data = member_fees[name].get(m, (0, 0))
original_expected = fee_data[0] if isinstance(fee_data, tuple) else fee_data
attendance_count = fee_data[1] if isinstance(fee_data, tuple) else 0
ex_data = exceptions.get((norm_name, norm_period))
if ex_data is not None:
expected = ex_data["amount"]
exception_info = ex_data
else:
expected = original_expected
exception_info = None
ledger[name][m] = {
"expected": member_fees[name].get(m, 0),
"expected": expected,
"original_expected": original_expected,
"attendance_count": attendance_count,
"exception": exception_info,
"paid": 0,
"transactions": [],
}
@@ -392,10 +450,12 @@ def print_report(result: dict, sorted_months: list[str]):
for m in sorted_months:
mdata = data["months"].get(m, {"expected": 0, "paid": 0})
expected = mdata["expected"]
original = mdata["original_expected"]
paid = int(mdata["paid"])
total_expected += expected
total_paid += paid
cell_status = ""
if expected == 0 and paid == 0:
cell = "-"
elif paid >= expected and expected > 0:
@@ -404,6 +464,7 @@ def print_report(result: dict, sorted_months: list[str]):
cell = f"{paid}/{expected}"
else:
cell = f"UNPAID {expected}"
member_balance += paid - expected
line += f" | {cell:>10}"
balance_str = f"{member_balance:+d}" if member_balance != 0 else "0"
@@ -509,7 +570,11 @@ def main():
print(f"Processing {len(transactions)} transactions.\n")
result = reconcile(members, sorted_months, transactions)
exceptions = fetch_exceptions(args.sheet_id, args.credentials)
if exceptions:
print(f"Loaded {len(exceptions)} fee exceptions.")
result = reconcile(members, sorted_months, transactions, exceptions)
print_report(result, sorted_months)