Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4bb8c7420c |
@@ -1,5 +1,7 @@
|
|||||||
# Antigravity Agent Configuration
|
# Antigravity Agent Configuration
|
||||||
# This file provides global rules for the Antigravity agent when working on this repository.
|
# 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:
|
- **Identity**: Antigravity AI (Assistant)
|
||||||
`Co-authored-by: Antigravity <antigravity@deepmind.com>`
|
- **Git Commits**: Always follow [Conventional Commits](https://www.conventionalcommits.org/) and append the co-author trailer:
|
||||||
|
`Co-authored-by: Antigravity <antigravity@google.com>`
|
||||||
|
- **Workflow**: Prefer updating `task.md` and `walkthrough.md` in the `.gemini/antigravity/brain/` directory to track progress and document changes.
|
||||||
|
|||||||
78
app.py
78
app.py
@@ -3,7 +3,10 @@ from pathlib import Path
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import re
|
import re
|
||||||
import time
|
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
|
# Add scripts directory to path to allow importing from it
|
||||||
scripts_dir = Path(__file__).parent / "scripts"
|
scripts_dir = Path(__file__).parent / "scripts"
|
||||||
@@ -14,6 +17,9 @@ from match_payments import reconcile, fetch_sheet_data, fetch_exceptions, normal
|
|||||||
|
|
||||||
app = Flask(__name__)
|
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
|
@app.before_request
|
||||||
def start_timer():
|
def start_timer():
|
||||||
g.start_time = time.perf_counter()
|
g.start_time = time.perf_counter()
|
||||||
@@ -141,20 +147,34 @@ def reconcile_view():
|
|||||||
for m in sorted_months:
|
for m in sorted_months:
|
||||||
mdata = data["months"].get(m, {"expected": 0, "original_expected": 0, "paid": 0})
|
mdata = data["months"].get(m, {"expected": 0, "original_expected": 0, "paid": 0})
|
||||||
expected = mdata["expected"]
|
expected = mdata["expected"]
|
||||||
original = mdata["original_expected"]
|
|
||||||
paid = int(mdata["paid"])
|
paid = int(mdata["paid"])
|
||||||
|
|
||||||
cell_status = ""
|
status = "empty"
|
||||||
if expected == 0 and paid == 0:
|
cell_text = "-"
|
||||||
cell = "-"
|
amount_to_pay = 0
|
||||||
elif paid >= expected and expected > 0:
|
|
||||||
cell = "OK"
|
|
||||||
elif paid > 0:
|
|
||||||
cell = f"{paid}/{expected}"
|
|
||||||
else:
|
|
||||||
cell = f"UNPAID {expected}"
|
|
||||||
|
|
||||||
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
|
row["balance"] = data["total_balance"] # Updated to use total_balance
|
||||||
formatted_results.append(row)
|
formatted_results.append(row)
|
||||||
@@ -178,7 +198,8 @@ def reconcile_view():
|
|||||||
debts=debts,
|
debts=debts,
|
||||||
unmatched=unmatched,
|
unmatched=unmatched,
|
||||||
attendance_url=attendance_url,
|
attendance_url=attendance_url,
|
||||||
payments_url=payments_url
|
payments_url=payments_url,
|
||||||
|
bank_account=BANK_ACCOUNT
|
||||||
)
|
)
|
||||||
|
|
||||||
@app.route("/payments")
|
@app.route("/payments")
|
||||||
@@ -221,5 +242,36 @@ def payments():
|
|||||||
payments_url=payments_url
|
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__":
|
if __name__ == "__main__":
|
||||||
app.run(debug=True, host='0.0.0.0', port=5001)
|
app.run(debug=True, host='0.0.0.0', port=5001)
|
||||||
|
|||||||
@@ -138,6 +138,28 @@
|
|||||||
|
|
||||||
.cell-unpaid {
|
.cell-unpaid {
|
||||||
color: #ff3333;
|
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 {
|
.cell-empty {
|
||||||
@@ -360,6 +382,42 @@
|
|||||||
margin-top: 5px;
|
margin-top: 5px;
|
||||||
color: #222;
|
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;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
@@ -403,8 +461,12 @@
|
|||||||
</td>
|
</td>
|
||||||
{% for cell in row.months %}
|
{% for cell in row.months %}
|
||||||
<td
|
<td
|
||||||
class="{% if cell == '-' %}cell-empty{% elif 'UNPAID' in cell %}cell-unpaid{% elif cell == 'OK' %}cell-ok{% endif %}">
|
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 }}
|
{{ cell.text }}
|
||||||
|
{% if cell.status == 'unpaid' or cell.status == 'partial' %}
|
||||||
|
<button class="pay-btn"
|
||||||
|
onclick="showPayQR('{{ row.name|e }}', {{ cell.amount }}, '{{ cell.month|e }}')">Pay</button>
|
||||||
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
<td class="{% if row.balance > 0 %}balance-pos{% elif row.balance < 0 %}balance-neg{% endif %}">
|
<td class="{% if row.balance > 0 %}balance-pos{% elif row.balance < 0 %}balance-neg{% endif %}">
|
||||||
@@ -460,6 +522,25 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- QR Code Modal -->
|
||||||
|
<div id="qrModal" class="modal"
|
||||||
|
style="display:none; position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; background-color: rgba(0, 0, 0, 0.9); z-index: 9999; justify-content: center; align-items: center;">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<div class="modal-title" id="qrTitle">Payment for ...</div>
|
||||||
|
<div class="close-btn" onclick="closeModal('qrModal')">[close]</div>
|
||||||
|
</div>
|
||||||
|
<div class="qr-image">
|
||||||
|
<img id="qrImg" src="" alt="Payment QR Code">
|
||||||
|
</div>
|
||||||
|
<div class="qr-details">
|
||||||
|
<div>Account: <span id="qrAccount"></span></div>
|
||||||
|
<div>Amount: <span id="qrAmount"></span> CZK</div>
|
||||||
|
<div>Message: <span id="qrMessage"></span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div id="memberModal">
|
<div id="memberModal">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
@@ -620,9 +701,16 @@
|
|||||||
document.getElementById('memberModal').classList.add('active');
|
document.getElementById('memberModal').classList.add('active');
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeModal() {
|
function closeModal(id) {
|
||||||
|
if (id) {
|
||||||
|
document.getElementById(id).style.display = 'none';
|
||||||
|
if (id === 'qrModal') {
|
||||||
|
document.getElementById(id).style.display = 'none';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
document.getElementById('memberModal').classList.remove('active');
|
document.getElementById('memberModal').classList.remove('active');
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Existing filter script
|
// Existing filter script
|
||||||
document.getElementById('nameFilter').addEventListener('input', function (e) {
|
document.getElementById('nameFilter').addEventListener('input', function (e) {
|
||||||
@@ -644,6 +732,33 @@
|
|||||||
document.addEventListener('keydown', function (e) {
|
document.addEventListener('keydown', function (e) {
|
||||||
if (e.key === 'Escape') closeModal();
|
if (e.key === 'Escape') closeModal();
|
||||||
});
|
});
|
||||||
|
function showPayQR(name, amount, month) {
|
||||||
|
const account = "{{ bank_account }}";
|
||||||
|
const message = `${name} / ${month}`;
|
||||||
|
const qrTitle = document.getElementById('qrTitle');
|
||||||
|
const qrImg = document.getElementById('qrImg');
|
||||||
|
const qrAccount = document.getElementById('qrAccount');
|
||||||
|
const qrAmount = document.getElementById('qrAmount');
|
||||||
|
const qrMessage = document.getElementById('qrMessage');
|
||||||
|
|
||||||
|
qrTitle.innerText = `Payment for ${month}`;
|
||||||
|
qrAccount.innerText = account;
|
||||||
|
qrAmount.innerText = amount;
|
||||||
|
qrMessage.innerText = message;
|
||||||
|
|
||||||
|
const encodedMessage = encodeURIComponent(message);
|
||||||
|
const qrUrl = `/qr?account=${encodeURIComponent(account)}&amount=${amount}&message=${encodedMessage}`;
|
||||||
|
|
||||||
|
qrImg.src = qrUrl;
|
||||||
|
document.getElementById('qrModal').style.display = 'block';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close modal when clicking outside
|
||||||
|
window.onclick = function (event) {
|
||||||
|
if (event.target.className === 'modal') {
|
||||||
|
event.target.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user