diff --git a/app/main.py b/app/main.py index 93bfa15..bac8473 100644 --- a/app/main.py +++ b/app/main.py @@ -782,8 +782,15 @@ def get_admin_venues(): ] -@app.get("/admin", response_class=HTMLResponse) +@app.get("/admin") def admin_page(request: Request): + if not require_admin(request): + return HTMLResponse("

Nicht erlaubt

", status_code=403) + return RedirectResponse("/admin/users", status_code=303) + + +@app.get("/admin/users", response_class=HTMLResponse) +def admin_users_page(request: Request): user = require_admin(request) if not user: @@ -812,15 +819,43 @@ def admin_page(request: Request): for row in rows ] - template = templates.get_template("admin.html") + template = templates.get_template("admin_users.html") return template.render( user=user, users=users, - venues=get_admin_venues(), invite_url=None, ) +@app.get("/admin/venues", response_class=HTMLResponse) +def admin_venues_page(request: Request): + user = require_admin(request) + if not user: + return HTMLResponse("

Nicht erlaubt

", status_code=403) + template = templates.get_template("admin_venues.html") + return template.render(user=user, venues=get_admin_venues()) + + +@app.get("/admin/patches", response_class=HTMLResponse) +def admin_patches_page(request: Request): + user = require_admin(request) + if not user: + return HTMLResponse("

Nicht erlaubt

", status_code=403) + assets = load_badge_assets() + patches = [ + { + "code": code, + "name": name, + "icon": icon, + "description": description, + "image_path": assets.get(code), + } + for code, name, icon, _threshold, description, _category in BADGE_DEFINITIONS + ] + template = templates.get_template("admin_patches.html") + return template.render(user=user, patches=patches) + + @app.post("/admin/invites", response_class=HTMLResponse) def create_invite(request: Request): user = require_admin(request) @@ -872,11 +907,10 @@ def create_invite(request: Request): } for row in rows ] - template = templates.get_template("admin.html") + template = templates.get_template("admin_users.html") return template.render( user=user, users=users, - venues=get_admin_venues(), invite_url=f"{str(request.base_url).rstrip('/')}/register/{token}", invite_id=invite_id, ) @@ -933,7 +967,7 @@ def update_venue( ) connection.commit() - return RedirectResponse("/admin#venues", status_code=303) + return RedirectResponse("/admin/venues", status_code=303) @app.post("/admin/venues/{venue_id}/merge") @@ -975,7 +1009,87 @@ def merge_venue( cursor.execute("DELETE FROM venues WHERE id = %s", (venue_id,)) connection.commit() - return RedirectResponse("/admin#venues", status_code=303) + return RedirectResponse("/admin/venues", status_code=303) + + +@app.post("/admin/venues/{venue_id}/delete") +def delete_venue(request: Request, venue_id: int): + if not require_admin(request): + return HTMLResponse("

Nicht erlaubt

", status_code=403) + + with get_db_connection() as connection: + with connection.cursor() as cursor: + cursor.execute("DELETE FROM venues WHERE id = %s", (venue_id,)) + connection.commit() + + return RedirectResponse("/admin/venues", status_code=303) + + +def remove_patch_file(path: str | None): + if not path: + return + filename = os.path.basename(path) + file_path = os.path.join(PATCH_DIR, filename) + if os.path.isfile(file_path): + os.remove(file_path) + + +@app.post("/admin/patches/{badge_code}") +async def update_patch_image( + request: Request, + badge_code: str, + image: UploadFile | None = File(None), +): + user = require_admin(request) + valid_codes = {definition[0] for definition in BADGE_DEFINITIONS} + if not user: + return HTMLResponse("

Nicht erlaubt

", status_code=403) + if badge_code not in valid_codes: + return HTMLResponse("

Patch nicht gefunden

", status_code=404) + + image_path, error = save_image(image, PATCH_DIR, "/static/uploads/patches/") + if error: + return error + if not image_path: + return HTMLResponse("

Bitte ein Bild auswählen.

", status_code=400) + + with get_db_connection() as connection: + with connection.cursor() as cursor: + cursor.execute("SELECT path FROM badge_assets WHERE badge_code = %s", (badge_code,)) + old_asset = cursor.fetchone() + cursor.execute( + """ + INSERT INTO badge_assets (badge_code, path, updated_by, updated_at) + VALUES (%s, %s, %s, CURRENT_TIMESTAMP) + ON CONFLICT (badge_code) DO UPDATE + SET path = EXCLUDED.path, updated_by = EXCLUDED.updated_by, + updated_at = CURRENT_TIMESTAMP + """, + (badge_code, image_path, user["id"]), + ) + connection.commit() + + if old_asset: + remove_patch_file(old_asset[0]) + return RedirectResponse("/admin/patches", status_code=303) + + +@app.post("/admin/patches/{badge_code}/delete") +def delete_patch_image(request: Request, badge_code: str): + if not require_admin(request): + return HTMLResponse("

Nicht erlaubt

", status_code=403) + if badge_code not in {definition[0] for definition in BADGE_DEFINITIONS}: + return HTMLResponse("

Patch nicht gefunden

", status_code=404) + + with get_db_connection() as connection: + with connection.cursor() as cursor: + cursor.execute("DELETE FROM badge_assets WHERE badge_code = %s RETURNING path", (badge_code,)) + deleted_asset = cursor.fetchone() + connection.commit() + + if deleted_asset: + remove_patch_file(deleted_asset[0]) + return RedirectResponse("/admin/patches", status_code=303) @app.post("/admin/users/{user_id}") @@ -1004,7 +1118,7 @@ def update_user_role( ) connection.commit() - return RedirectResponse("/admin", status_code=303) + return RedirectResponse("/admin/users", status_code=303) @app.post("/admin/users/{user_id}/delete") @@ -1024,7 +1138,7 @@ def delete_user(request: Request, user_id: int): cursor.execute("DELETE FROM users WHERE id = %s", (user_id,)) connection.commit() - return RedirectResponse("/admin", status_code=303) + return RedirectResponse("/admin/users", status_code=303) @app.post("/register") def register_user( diff --git a/app/static/css/style.css b/app/static/css/style.css index 010a072..0824b01 100644 --- a/app/static/css/style.css +++ b/app/static/css/style.css @@ -263,15 +263,24 @@ form .button { display: block; } -.venue-results { +.venue-search-control { position: relative; +} + +.venue-results { + position: absolute; + top: calc(100% + 5px); + left: 0; + right: 0; background: var(--surface); border: 1px solid var(--border); border-radius: 10px; - margin-top: 5px; - overflow: hidden; + margin-top: 0; + max-height: min(55vh, 420px); + overflow-y: auto; + overscroll-behavior: contain; z-index: 50; } @@ -301,6 +310,50 @@ form .button { } } +/* Admin */ +.admin-nav { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin-bottom: 24px; +} + +.admin-nav a { + padding: 10px 14px; + color: var(--muted); + background: var(--surface); + border: 1px solid var(--border); + border-radius: 9px; + text-decoration: none; +} + +.admin-nav a:hover, +.admin-nav a.active { + color: var(--text); + border-color: var(--accent); + background: var(--surface-hover); +} + +.admin-section { + margin-bottom: 28px; + padding: 24px; + background: var(--surface); + border: 1px solid var(--border); + border-radius: 16px; +} + +.admin-meta { color: var(--muted); font-size: .9rem; } +.admin-list { display: grid; gap: 12px; } +.admin-row { padding: 16px; background: #0f1420; border: 1px solid var(--border); border-radius: 10px; } +.admin-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; } +.admin-danger { padding: 9px 12px; color: #fecaca; border: 1px solid #7f1d1d; background: #450a0a; border-radius: 8px; cursor: pointer; } +.admin-danger:disabled { opacity: .4; cursor: not-allowed; } + +@media (max-width: 700px) { + .admin-nav { display: grid; grid-template-columns: 1fr; } + .admin-section { padding: 16px; } +} + .venue-result { width: 100%; display: flex; diff --git a/app/templates/_admin_nav.html b/app/templates/_admin_nav.html new file mode 100644 index 0000000..7104a43 --- /dev/null +++ b/app/templates/_admin_nav.html @@ -0,0 +1,5 @@ + diff --git a/app/templates/admin.html b/app/templates/admin.html deleted file mode 100644 index 67c223c..0000000 --- a/app/templates/admin.html +++ /dev/null @@ -1,118 +0,0 @@ - - - - - - Verwaltung · Pingu Concerts - - - - -
-
- - {{ user.display_name }} -
-
-
-
-

⚙️ Verwaltung

-

Einladungen und Benutzerkonten verwalten.

-
-
-

Einladung erstellen

-

Der Link kann einmalig verwendet werden, um einen neuen Account anzulegen.

-
- -
- {% if invite_url %} - - {% endif %} -
-
-

Benutzer

-
- {% for managed_user in users %} -
-
- {{ managed_user.display_name }} -
@{{ managed_user.username }} · {{ managed_user.email }} · seit {{ managed_user.created_at }}
-
-
- - -
- {% if managed_user.id != user.id %} -
- -
- {% endif %} -
- {% endfor %} -
-
-
-

Veranstaltungsorte

-

Namen und Adressen korrigieren, Such-Aliase ergänzen oder Dubletten zusammenführen.

-
- {% for venue in venues %} -
-
-
- - - - - -
- -
- - Quelle: {{ venue.source }} · {{ venue.concert_count }} Konzert(e) · ID {{ venue.id }} - -
-
- {% if venues|length > 1 %} -
- - -
- {% endif %} -
- {% else %} -

Noch keine Veranstaltungsorte vorhanden.

- {% endfor %} -
-
-
- - diff --git a/app/templates/admin_patches.html b/app/templates/admin_patches.html new file mode 100644 index 0000000..070fd79 --- /dev/null +++ b/app/templates/admin_patches.html @@ -0,0 +1,38 @@ + + + + + + Patch-Bilder · Pingu Concerts + + + + +
{{ user.display_name }}
+
+

🧵 Patch-Bilder

Grafiken für die Patches auf der virtuellen Kutte verwalten.

+ {% set admin_section = 'patches' %}{% include '_admin_nav.html' %} +
+
+ {% for patch in patches %} +
+ {% if patch.image_path %}{{ patch.name }}{% else %}
{{ patch.icon }}
{% endif %} +
{{ patch.name }}
{{ patch.description }} · {{ patch.code }}
+
+ + +
+ {% if patch.image_path %}
{% endif %} +
+ {% endfor %} +
+
+
+ + diff --git a/app/templates/admin_users.html b/app/templates/admin_users.html new file mode 100644 index 0000000..0b71314 --- /dev/null +++ b/app/templates/admin_users.html @@ -0,0 +1,44 @@ + + + + + + Benutzerverwaltung · Pingu Concerts + + + + +
{{ user.display_name }}
+
+

⚙️ Verwaltung

Benutzerkonten und Einladungen verwalten.

+ {% set admin_section = 'users' %}{% include '_admin_nav.html' %} +
+

Einladung erstellen

+

Der Link kann einmalig verwendet werden.

+
+ {% if invite_url %}{% endif %} +
+
+

Benutzer

+
+ {% for managed_user in users %} +
+
{{ managed_user.display_name }}
@{{ managed_user.username }} · {{ managed_user.email }} · seit {{ managed_user.created_at }}
+
+ + +
+ {% if managed_user.id != user.id %}
{% endif %} +
+ {% endfor %} +
+
+
+ + diff --git a/app/templates/admin_venues.html b/app/templates/admin_venues.html new file mode 100644 index 0000000..cb3f5f2 --- /dev/null +++ b/app/templates/admin_venues.html @@ -0,0 +1,59 @@ + + + + + + Veranstaltungsorte · Pingu Concerts + + + + +
{{ user.display_name }}
+
+

📍 Veranstaltungsorte

Orte korrigieren, bestätigen, zusammenführen oder entfernen.

+ {% set admin_section = 'venues' %}{% include '_admin_nav.html' %} +
+
+ {% for venue in venues %} +
+
+
+ + + + + +
+ +
+ + Quelle: {{ venue.source }} · {{ venue.concert_count }} Konzert(e) · ID {{ venue.id }} + +
+
+ {% if venues|length > 1 %} +
+ + +
+ {% endif %} +
+ +
+
+ {% else %}

Noch keine Veranstaltungsorte vorhanden.

{% endfor %} +
+
+
+ + diff --git a/app/templates/new_concert.html b/app/templates/new_concert.html index 8917f88..dcb20e4 100644 --- a/app/templates/new_concert.html +++ b/app/templates/new_concert.html @@ -135,19 +135,21 @@ Veranstaltungsort - +
+ -
+
+