From 4bb8c7420ce812f3caacaa7ef07d9199b6a78e84 Mon Sep 17 00:00:00 2001 From: Jan Novak Date: Mon, 2 Mar 2026 22:54:48 +0100 Subject: [PATCH] feat: implement local payment QR codes and update AI co-authoring rules QR codes are now generated locally using the 'qrcode' library for better privacy and reliability. Updated .agent/rules.md with co-author details and Conventional Commits preference. Co-authored-by: Antigravity --- .agent/rules.md | 6 +- app.py | 78 ++++++++++++++++++++----- templates/reconcile.html | 123 +++++++++++++++++++++++++++++++++++++-- 3 files changed, 188 insertions(+), 19 deletions(-) diff --git a/.agent/rules.md b/.agent/rules.md index 634c21c..fa8ee51 100644 --- a/.agent/rules.md +++ b/.agent/rules.md @@ -1,5 +1,7 @@ # Antigravity Agent Configuration # This file provides global rules for the Antigravity agent when working on this repository. -- **Git Commits**: When making git commits, always append the following co-author trailer to the end of the commit message to indicate AI assistance: - `Co-authored-by: Antigravity ` +- **Identity**: Antigravity AI (Assistant) +- **Git Commits**: Always follow [Conventional Commits](https://www.conventionalcommits.org/) and append the co-author trailer: + `Co-authored-by: Antigravity ` +- **Workflow**: Prefer updating `task.md` and `walkthrough.md` in the `.gemini/antigravity/brain/` directory to track progress and document changes. diff --git a/app.py b/app.py index 3a4ac11..00d8b41 100644 --- a/app.py +++ b/app.py @@ -3,7 +3,10 @@ from pathlib import Path from datetime import datetime import re import time -from flask import Flask, render_template, g +import os +import io +import qrcode +from flask import Flask, render_template, g, send_file, request # Add scripts directory to path to allow importing from it scripts_dir = Path(__file__).parent / "scripts" @@ -14,6 +17,9 @@ from match_payments import reconcile, fetch_sheet_data, fetch_exceptions, normal app = Flask(__name__) +# Bank account for QR code payments (can be overridden by ENV) +BANK_ACCOUNT = os.environ.get("BANK_ACCOUNT", "CZ8520100000002800359168") + @app.before_request def start_timer(): g.start_time = time.perf_counter() @@ -141,20 +147,34 @@ def reconcile_view(): for m in sorted_months: mdata = data["months"].get(m, {"expected": 0, "original_expected": 0, "paid": 0}) expected = mdata["expected"] - original = mdata["original_expected"] paid = int(mdata["paid"]) - cell_status = "" - if expected == 0 and paid == 0: - cell = "-" - elif paid >= expected and expected > 0: - cell = "OK" - elif paid > 0: - cell = f"{paid}/{expected}" - else: - cell = f"UNPAID {expected}" + status = "empty" + cell_text = "-" + amount_to_pay = 0 - row["months"].append(cell) + if expected > 0: + if paid >= expected: + status = "ok" + cell_text = "OK" + elif paid > 0: + status = "partial" + cell_text = f"{paid}/{expected}" + amount_to_pay = expected - paid + else: + status = "unpaid" + cell_text = f"UNPAID {expected}" + amount_to_pay = expected + elif paid > 0: + status = "surplus" + cell_text = f"PAID {paid}" + + row["months"].append({ + "text": cell_text, + "status": status, + "amount": amount_to_pay, + "month": month_labels[m] + }) row["balance"] = data["total_balance"] # Updated to use total_balance formatted_results.append(row) @@ -178,7 +198,8 @@ def reconcile_view(): debts=debts, unmatched=unmatched, attendance_url=attendance_url, - payments_url=payments_url + payments_url=payments_url, + bank_account=BANK_ACCOUNT ) @app.route("/payments") @@ -221,5 +242,36 @@ def payments(): payments_url=payments_url ) +@app.route("/qr") +def qr_code(): + account = request.args.get("account", BANK_ACCOUNT) + amount = request.args.get("amount", "0") + message = request.args.get("message", "") + + # QR Platba standard: SPD*1.0*ACC:accountNumber*BC:bankCode*AM:amount*CC:CZK*MSG:message + acc_parts = account.split('/') + if len(acc_parts) == 2: + acc_str = f"{acc_parts[0]}*BC:{acc_parts[1]}" + else: + acc_str = account + + try: + amt_val = float(amount) + amt_str = f"{amt_val:.2f}" + except ValueError: + amt_str = "0.00" + + # Message max 60 characters + msg_str = message[:60] + + qr_data = f"SPD*1.0*ACC:{acc_str}*AM:{amt_str}*CC:CZK*MSG:{msg_str}" + + img = qrcode.make(qr_data) + buf = io.BytesIO() + img.save(buf, format='PNG') + buf.seek(0) + + return send_file(buf, mimetype='image/png') + if __name__ == "__main__": app.run(debug=True, host='0.0.0.0', port=5001) diff --git a/templates/reconcile.html b/templates/reconcile.html index 6b97aa5..36374f1 100644 --- a/templates/reconcile.html +++ b/templates/reconcile.html @@ -138,6 +138,28 @@ .cell-unpaid { color: #ff3333; + background-color: rgba(255, 51, 51, 0.05); + position: relative; + } + + .pay-btn { + display: none; + position: absolute; + right: 5px; + top: 50%; + transform: translateY(-50%); + background: #ff3333; + color: white; + border: none; + border-radius: 3px; + padding: 2px 6px; + font-size: 10px; + cursor: pointer; + font-weight: bold; + } + + .member-row:hover .pay-btn { + display: inline-block; } .cell-empty { @@ -360,6 +382,42 @@ margin-top: 5px; color: #222; } + + /* QR Modal styles */ + #qrModal .modal-content { + max-width: 400px; + text-align: center; + } + + .qr-image { + background: white; + padding: 10px; + border-radius: 5px; + margin: 20px 0; + display: inline-block; + } + + .qr-image img { + display: block; + width: 250px; + height: 250px; + } + + .qr-details { + text-align: left; + margin-top: 15px; + font-size: 14px; + color: #ccc; + } + + .qr-details div { + margin-bottom: 5px; + } + + .qr-details span { + color: #00ff00; + font-family: monospace; + } @@ -403,8 +461,12 @@ {% for cell in row.months %} - {{ cell }} + class="{% if cell.status == 'empty' %}cell-empty{% elif cell.status == 'unpaid' or cell.status == 'partial' %}cell-unpaid{% elif cell.status == 'ok' %}cell-ok{% endif %}"> + {{ cell.text }} + {% if cell.status == 'unpaid' or cell.status == 'partial' %} + + {% endif %} {% endfor %} @@ -460,6 +522,25 @@ {% endif %} + + +