Add transaction history
Test FastAPI Startup / fastapi-up (push) Has been cancelled

Co-authored-by: Sebastian Beckmann <beckmann.sebastian@outlook.de>
This commit is contained in:
2026-07-03 18:07:31 +02:00
co-authored by Sebastian Beckmann
parent daa2d80397
commit 9f918c40e0
5 changed files with 165 additions and 24 deletions
+47 -12
View File
@@ -77,18 +77,36 @@ with engine.connect() as conn:
"""))
# create a table for every push on the drink button
#conn.execute(text("""ALTER TABLE drinks ADD transaction_id INTEGER"""))
#conn.execute(text("""DROP TABLE drinks_temp"""))
#conn.execute(text("""
# CREATE TABLE IF NOT EXISTS drinks_temp (
# id INTEGER PRIMARY KEY AUTOINCREMENT,
# postpaid_user_id INTEGER,
# prepaid_user_id INTEGER,
# timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
# drink_type INTEGER DEFAULT 1,
# transaction_id INTEGER
# )
# """))
#conn.execute(text("""INSERT INTO drinks_temp SELECT * FROM drinks"""))
#conn.execute(text("""DROP TABLE drinks"""))
conn.execute(text("""
CREATE TABLE IF NOT EXISTS drinks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
postpaid_user_id INTEGER,
prepaid_user_id INTEGER,
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
drink_type INTEGER DEFAULT 1,
FOREIGN KEY (postpaid_user_id) REFERENCES users_postpaid(id),
FOREIGN KEY (prepaid_user_id) REFERENCES users_prepaid(id),
FOREIGN KEY (drink_type) REFERENCES drink_types(id)
id INTEGER PRIMARY KEY AUTOINCREMENT,
postpaid_user_id INTEGER,
prepaid_user_id INTEGER,
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
drink_type INTEGER DEFAULT 1,
transaction_id INTEGER,
FOREIGN KEY (postpaid_user_id) REFERENCES users_postpaid(id),
FOREIGN KEY (prepaid_user_id) REFERENCES users_prepaid(id),
FOREIGN KEY (drink_type) REFERENCES drink_types(id),
FOREIGN KEY (transaction_id) REFERENCES transactions(id)
)
"""))
#conn.execute(text("""INSERT INTO drinks SELECT * FROM drinks_temp"""))
#conn.execute(text("""DROP TABLE drinks_temp"""))
# create a table for every money transaction
conn.execute(text("""
@@ -136,7 +154,8 @@ def _log_transaction(
previous_money_cent = None,
new_money_cent = None,
delta_money_cent = None,
description = None
description = None,
transaction_type = None
):
"""
Logs a transaction for a user, recording changes in their account balance.
@@ -328,7 +347,7 @@ def drink_postpaid_user(user_id: int, drink_type: int = 1):
raise HTTPException(status_code=403, detail="User not activated")
prev_money = get_postpaid_user(user_id)["money"]
_log_transaction(
transaction_id = _log_transaction(
user_id=user_id,
user_is_postpaid=True,
previous_money_cent=prev_money,
@@ -342,11 +361,11 @@ def drink_postpaid_user(user_id: int, drink_type: int = 1):
raise HTTPException(status_code=404, detail="User not found")
connection.commit()
t_without_drink_type = text("INSERT INTO drinks (postpaid_user_id, timestamp) VALUES (:postpaid_user_id, CURRENT_TIMESTAMP)")
t_without_drink_type = text("INSERT INTO drinks (postpaid_user_id, timestamp, transaction_id) VALUES (:postpaid_user_id, CURRENT_TIMESTAMP, :transaction_id)")
t_update_drink_types = text("UPDATE drink_types SET quantity = quantity - 1 WHERE id = 1")
with engine.connect() as connection:
result = connection.execute(t_without_drink_type, {"postpaid_user_id": user_id})
result = connection.execute(t_without_drink_type, {"postpaid_user_id": user_id, "transaction_id": transaction_id})
if result.rowcount == 0:
raise HTTPException(status_code=500, detail="Failed to create drink entry")
result2 = connection.execute(t_update_drink_types)
@@ -942,3 +961,19 @@ def get_stats_drink_hourly():
hourly_stats.sort(key=lambda x: x["hour"])
return hourly_stats
def get_drink_history(user_id: int, limit: int = 10):
"""
Retrieve the drink history for the current user, sorted by timestamp in descending order. Limited to the specified number of entries. Only usable for postpaid users as the current user.
Args:
user_id (int): id of postpaid user
limit (int): max number of entries for history. Default: 10
Returns:
list[dict]: A list containing the transaction_id, timestamp, money after the transaction, difference to the
last entry, description of the transaction and a path to an icon containing the image of the drink,
if this transaction was a drink.
"""
t2 = text("SELECT t.id, t.timestamp, t.new_money, t.delta_money, t.description, i.icon FROM transactions as t LEFT JOIN drinks ON t.id = drinks.transaction_id LEFT JOIN drink_types as i ON i.id = drinks.drink_type WHERE t.postpaid_user_id = :user_id ORDER BY t.timestamp DESC LIMIT :limit")
with engine.connect() as connection:
result = connection.execute(t2, {"user_id": user_id, "limit": limit}).fetchall()
return result
+5
View File
@@ -99,6 +99,10 @@ def home(request: Request):
most_used_drinks = db.models.get_most_used_drinks(user_db_id, user_is_postpaid, 99)
most_used_drinks.append({"drink_type_id": 1, "drink_type": "Sonstiges", "count": 0, "icon": "sonstiges.png"}) # ensure "Sonstiges" is in
drink_history = []
if user_is_postpaid:
drink_history = db.models.get_drink_history(user_db_id, 100)
return templates.TemplateResponse("index.html", {
"request": request,
@@ -112,6 +116,7 @@ def home(request: Request):
"last_regular_drink": last_regular_drink,
"avail_drink_types": most_used_drinks,
"drink_types": drink_types,
"drink_history": drink_history,
})
@app.get("/login", response_class=HTMLResponse)
+31
View File
@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<svg version="1.1" id="main_outline" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px"
y="0px" viewBox="0 0 640 640" style="enable-background:new 0 0 640 640;" xml:space="preserve">
<g>
<path id="teabag" style="fill:#FFFFFF" d="M395.9,484.2l-126.9-61c-12.5-6-17.9-21.2-11.8-33.8l61-126.9c6-12.5,21.2-17.9,33.8-11.8
c17.2,8.3,27.1,13,27.1,13l-0.1-109.2l16.7-0.1l0.1,117.1c0,0,57.4,24.2,83.1,40.1c3.7,2.3,10.2,6.8,12.9,14.4
c2.1,6.1,2,13.1-1,19.3l-61,126.9C423.6,484.9,408.4,490.3,395.9,484.2z"/>
<g>
<g>
<path style="fill:#609926" d="M622.7,149.8c-4.1-4.1-9.6-4-9.6-4s-117.2,6.6-177.9,8c-13.3,0.3-26.5,0.6-39.6,0.7c0,39.1,0,78.2,0,117.2
c-5.5-2.6-11.1-5.3-16.6-7.9c0-36.4-0.1-109.2-0.1-109.2c-29,0.4-89.2-2.2-89.2-2.2s-141.4-7.1-156.8-8.5
c-9.8-0.6-22.5-2.1-39,1.5c-8.7,1.8-33.5,7.4-53.8,26.9C-4.9,212.4,6.6,276.2,8,285.8c1.7,11.7,6.9,44.2,31.7,72.5
c45.8,56.1,144.4,54.8,144.4,54.8s12.1,28.9,30.6,55.5c25,33.1,50.7,58.9,75.7,62c63,0,188.9-0.1,188.9-0.1s12,0.1,28.3-10.3
c14-8.5,26.5-23.4,26.5-23.4s12.9-13.8,30.9-45.3c5.5-9.7,10.1-19.1,14.1-28c0,0,55.2-117.1,55.2-231.1
C633.2,157.9,624.7,151.8,622.7,149.8z M125.6,353.9c-25.9-8.5-36.9-18.7-36.9-18.7S69.6,321.8,60,295.4
c-16.5-44.2-1.4-71.2-1.4-71.2s8.4-22.5,38.5-30c13.8-3.7,31-3.1,31-3.1s7.1,59.4,15.7,94.2c7.2,29.2,24.8,77.7,24.8,77.7
S142.5,359.9,125.6,353.9z M425.9,461.5c0,0-6.1,14.5-19.6,15.4c-5.8,0.4-10.3-1.2-10.3-1.2s-0.3-0.1-5.3-2.1l-112.9-55
c0,0-10.9-5.7-12.8-15.6c-2.2-8.1,2.7-18.1,2.7-18.1L322,273c0,0,4.8-9.7,12.2-13c0.6-0.3,2.3-1,4.5-1.5c8.1-2.1,18,2.8,18,2.8
l110.7,53.7c0,0,12.6,5.7,15.3,16.2c1.9,7.4-0.5,14-1.8,17.2C474.6,363.8,425.9,461.5,425.9,461.5z"/>
<path style="fill:#609926" d="M326.8,380.1c-8.2,0.1-15.4,5.8-17.3,13.8c-1.9,8,2,16.3,9.1,20c7.7,4,17.5,1.8,22.7-5.4
c5.1-7.1,4.3-16.9-1.8-23.1l24-49.1c1.5,0.1,3.7,0.2,6.2-0.5c4.1-0.9,7.1-3.6,7.1-3.6c4.2,1.8,8.6,3.8,13.2,6.1
c4.8,2.4,9.3,4.9,13.4,7.3c0.9,0.5,1.8,1.1,2.8,1.9c1.6,1.3,3.4,3.1,4.7,5.5c1.9,5.5-1.9,14.9-1.9,14.9
c-2.3,7.6-18.4,40.6-18.4,40.6c-8.1-0.2-15.3,5-17.7,12.5c-2.6,8.1,1.1,17.3,8.9,21.3c7.8,4,17.4,1.7,22.5-5.3
c5-6.8,4.6-16.3-1.1-22.6c1.9-3.7,3.7-7.4,5.6-11.3c5-10.4,13.5-30.4,13.5-30.4c0.9-1.7,5.7-10.3,2.7-21.3
c-2.5-11.4-12.6-16.7-12.6-16.7c-12.2-7.9-29.2-15.2-29.2-15.2s0-4.1-1.1-7.1c-1.1-3.1-2.8-5.1-3.9-6.3c4.7-9.7,9.4-19.3,14.1-29
c-4.1-2-8.1-4-12.2-6.1c-4.8,9.8-9.7,19.7-14.5,29.5c-6.7-0.1-12.9,3.5-16.1,9.4c-3.4,6.3-2.7,14.1,1.9,19.8
C343.2,346.5,335,363.3,326.8,380.1z"/>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

+7 -10
View File
@@ -24,16 +24,13 @@
fill: #0366d6;
}
</style>
<a class="github-icon-link" href="https://github.com/Moritz921/GetraenkelisteWebsite" target="_blank" title="GitHub" rel="noopener">
<svg viewBox="0 0 16 16" aria-hidden="true">
<path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38
0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52
-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2
-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64
-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08
2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01
1.93-.01 2.19 0 .21.15.46.55.38A8.013 8.013 0 0 0 16 8c0-4.42-3.58-8-8-8z"/>
</svg>
<a class="github-icon-link" href="https://git.fs.cs.uni-frankfurt.de/Fachschaft/GetraenkelisteWebsite" target="_blank" title="Gitea" rel="noopener">
<img
src="/static/gitea.svg"
alt="Gitea Logo"
height="64"
width="64"
/>
</a>
</head>
<body>
+75 -2
View File
@@ -242,7 +242,81 @@ content %}
<p>Es sind keine Prepaid-User vorhanden.</p>
{% endif %}
{% endif %}
{% if 'Getraenkeliste Verantwortliche' in user.groups %}
{% if drink_history %}
<style>
.hidden {
display: none
}
</style>
<h3>Historie</h3>
<table>
<thead>
<tr>
<th style="padding: 0.5em 1em">Datum / Uhrzeit</th>
<th style="padding: 0.5em 1em">Veränderung</th>
<th style="padding: 0.5em 1em">Neuer Kontostand</th>
<th style="padding: 0.5em 1em">Transaktionsart</th>
<th style="padding: 0.5em 1em">Icon</th>
</tr>
</thead>
<tbody>
{% for drink in drink_history %}
{% if loop.index <= 10 %}
<tr>
{% else %}
<tr class="long-history-hidden hidden">
{% endif %}
<td style="padding: 0.5em 1em">{{ drink.timestamp }} UTC</td>
<td style="padding: 0.5em 1em">
{% if drink.delta_money > 0 %}
+{{ drink.delta_money / 100}} €
{% else %}
{{ drink.delta_money / 100}} €
{% endif %}
</td>
<td style="padding: 0.5em 1em">{{ drink.new_money / 100}} €</td>
<td style="padding: 0.5em 1em">
{% if drink.description == "Drink button pressed" %}
Strich hinzugefügt
{% elif drink.description == "Reverted last drink" %}
Strich rückgängig gemacht
{% elif drink.description == "Set money manually via Admin UI" %}
Änderung durch Admin
{% else %}
{{ drink.description }}
{% endif %}
</td>
<td style="padding: 0.5em 1em">
{% if drink.icon %}
<img src="/static/drinks/{{ drink.icon }}" alt="{{ drink.icon }}" style="width:48px; height:48px; object-fit:contain;">
{% elif drink.description == "Drink button pressed" %}
:(
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
<div class="long-history-hidden hidden">Es werden maximal 100 Ergebnisse angezeigt.</div>
<button id="history-expand" class="long-history-hidden-inv">Mehr anzeigen</button>
<button id="history-collapse" class="long-history-hidden hidden" type="button">Weniger anzeigen</button>
<script>
const hidden = () => document.querySelectorAll('.long-history-hidden');
const hidden_inv = () => document.querySelectorAll('.long-history-hidden-inv');
document.getElementById('history-expand').addEventListener('click', () => {
hidden().forEach(el => el.classList.remove('hidden'));
hidden_inv().forEach(el => el.classList.add('hidden'));
});
document.getElementById('history-collapse').addEventListener('click', () => {
hidden().forEach(el => el.classList.add('hidden'));
hidden_inv().forEach(el => el.classList.remove('hidden'));
});
</script>
{% endif %}
{% if 'Getraenkeliste Verantwortliche' in user.groups %}
<h2>Admin Interface</h2>
<p>Ausgleichszahlung:</p>
<p>Der eingegebene Betrag wird vom aktuell eingeloggten Nutzer abgezogen und dem eingetragenem Nutzer gutgeschrieben.</p>
@@ -403,7 +477,6 @@ content %}
<input id="drink_type_quantity" type="number" name="quantity" min="0" step="1" required style="padding: 0.5em; border: 1px solid #ccc; border-radius: 4px; width: 100px;" />
<button type="submit" style="padding: 0.5em 1em; background: rgb(0, 97, 143); color: #fff; border: none; border-radius: 4px; cursor: pointer;">Setzen</button>
</form>
{% endif %}
{% endif %}