bug fix zu adresse
This commit is contained in:
+123
-9
@@ -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("<h1>Nicht erlaubt</h1>", 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("<h1>Nicht erlaubt</h1>", 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("<h1>Nicht erlaubt</h1>", 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("<h1>Nicht erlaubt</h1>", 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("<h1>Nicht erlaubt</h1>", status_code=403)
|
||||
if badge_code not in valid_codes:
|
||||
return HTMLResponse("<h1>Patch nicht gefunden</h1>", status_code=404)
|
||||
|
||||
image_path, error = save_image(image, PATCH_DIR, "/static/uploads/patches/")
|
||||
if error:
|
||||
return error
|
||||
if not image_path:
|
||||
return HTMLResponse("<h1>Bitte ein Bild auswählen.</h1>", 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("<h1>Nicht erlaubt</h1>", status_code=403)
|
||||
if badge_code not in {definition[0] for definition in BADGE_DEFINITIONS}:
|
||||
return HTMLResponse("<h1>Patch nicht gefunden</h1>", 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(
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
<nav class="admin-nav" aria-label="Adminbereiche">
|
||||
<a href="/admin/users" class="{% if admin_section == 'users' %}active{% endif %}">👥 Benutzer</a>
|
||||
<a href="/admin/venues" class="{% if admin_section == 'venues' %}active{% endif %}">📍 Veranstaltungsorte</a>
|
||||
<a href="/admin/patches" class="{% if admin_section == 'patches' %}active{% endif %}">🧵 Patch-Bilder</a>
|
||||
</nav>
|
||||
@@ -1,118 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Verwaltung · Pingu Concerts</title>
|
||||
<link rel="stylesheet" href="/static/css/style.css">
|
||||
<style>
|
||||
.admin-section { margin: 0 0 28px; padding: 24px; background: var(--surface); border: 1px solid var(--border); border-radius: 16px; }
|
||||
.invite-link { display: block; margin-top: 14px; padding: 12px; overflow-wrap: anywhere; background: #0f1420; border: 1px solid var(--border); border-radius: 9px; color: #c4b5fd; }
|
||||
.user-list { display: grid; gap: 12px; }
|
||||
.user-row { display: grid; grid-template-columns: minmax(0, 1fr) auto auto; gap: 12px; align-items: center; padding: 16px; background: #0f1420; border: 1px solid var(--border); border-radius: 10px; }
|
||||
.user-meta { color: var(--muted); font-size: .9rem; margin-top: 4px; }
|
||||
.role-form, .delete-form { margin: 0; display: flex; align-items: center; gap: 8px; }
|
||||
.role-form select { padding: 9px; background: #0b0f19; color: var(--text); border: 1px solid var(--border); border-radius: 8px; }
|
||||
.delete-button { padding: 9px 12px; color: #fecaca; border: 1px solid #7f1d1d; background: #450a0a; border-radius: 8px; cursor: pointer; }
|
||||
.venue-list { display: grid; gap: 14px; }
|
||||
.venue-card { padding: 16px; background: #0f1420; border: 1px solid var(--border); border-radius: 10px; }
|
||||
.venue-fields { display: grid; grid-template-columns: 2fr 2fr .8fr 1.4fr 1.2fr; gap: 8px; }
|
||||
.venue-fields input, .merge-form select { min-width: 0; width: 100%; }
|
||||
.venue-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 12px; margin-top: 10px; }
|
||||
.venue-actions label { display: flex; align-items: center; gap: 6px; }
|
||||
.venue-aliases { margin-top: 8px; }
|
||||
.merge-form { display: grid; grid-template-columns: minmax(180px, 1fr) auto; gap: 8px; margin-top: 10px; padding-top: 10px; border-top: 1px solid var(--border); }
|
||||
@media (max-width: 700px) { .user-row { grid-template-columns: 1fr; } .role-form, .delete-form { width: 100%; } }
|
||||
@media (max-width: 900px) { .venue-fields { grid-template-columns: 1fr 1fr; } }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<div class="header-inner">
|
||||
<a href="/" class="logo">🎸 Pingu <span>Concerts</span></a>
|
||||
<span>{{ user.display_name }}</span>
|
||||
</div>
|
||||
</header>
|
||||
<main>
|
||||
<div class="page-title">
|
||||
<h1>⚙️ Verwaltung</h1>
|
||||
<p>Einladungen und Benutzerkonten verwalten.</p>
|
||||
</div>
|
||||
<section class="admin-section">
|
||||
<h2>Einladung erstellen</h2>
|
||||
<p>Der Link kann einmalig verwendet werden, um einen neuen Account anzulegen.</p>
|
||||
<form method="post" action="/admin/invites">
|
||||
<button class="button" type="submit">+ Einladungslink erzeugen</button>
|
||||
</form>
|
||||
{% if invite_url %}
|
||||
<label class="invite-link">{{ invite_url }}</label>
|
||||
{% endif %}
|
||||
</section>
|
||||
<section class="admin-section">
|
||||
<h2>Benutzer</h2>
|
||||
<div class="user-list">
|
||||
{% for managed_user in users %}
|
||||
<div class="user-row">
|
||||
<div>
|
||||
<strong>{{ managed_user.display_name }}</strong>
|
||||
<div class="user-meta">@{{ managed_user.username }} · {{ managed_user.email }} · seit {{ managed_user.created_at }}</div>
|
||||
</div>
|
||||
<form class="role-form" method="post" action="/admin/users/{{ managed_user.id }}">
|
||||
<select name="role" aria-label="Rolle für {{ managed_user.username }}">
|
||||
<option value="user" {% if not managed_user.is_admin %}selected{% endif %}>Benutzer</option>
|
||||
<option value="admin" {% if managed_user.is_admin %}selected{% endif %}>Admin</option>
|
||||
</select>
|
||||
<button class="button" type="submit">Speichern</button>
|
||||
</form>
|
||||
{% if managed_user.id != user.id %}
|
||||
<form class="delete-form" method="post" action="/admin/users/{{ managed_user.id }}/delete" onsubmit="return confirm('Diesen Benutzer wirklich löschen?');">
|
||||
<button class="delete-button" type="submit">Löschen</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</section>
|
||||
<section class="admin-section" id="venues">
|
||||
<h2>Veranstaltungsorte</h2>
|
||||
<p>Namen und Adressen korrigieren, Such-Aliase ergänzen oder Dubletten zusammenführen.</p>
|
||||
<div class="venue-list">
|
||||
{% for venue in venues %}
|
||||
<div class="venue-card">
|
||||
<form method="post" action="/admin/venues/{{ venue.id }}">
|
||||
<div class="venue-fields">
|
||||
<input name="name" value="{{ venue.name }}" aria-label="Name" required>
|
||||
<input name="street" value="{{ venue.street }}" placeholder="Straße" aria-label="Straße">
|
||||
<input name="postal_code" value="{{ venue.postal_code }}" placeholder="PLZ" aria-label="PLZ">
|
||||
<input name="city" value="{{ venue.city }}" placeholder="Ort" aria-label="Ort">
|
||||
<input name="country" value="{{ venue.country }}" placeholder="Land" aria-label="Land">
|
||||
</div>
|
||||
<input class="venue-aliases" name="aliases" value="{{ venue.aliases }}" placeholder="Aliase, durch Komma getrennt" aria-label="Aliase">
|
||||
<div class="venue-actions">
|
||||
<label><input type="checkbox" name="is_verified" {% if venue.is_verified %}checked{% endif %}> bestätigt</label>
|
||||
<span class="user-meta">Quelle: {{ venue.source }} · {{ venue.concert_count }} Konzert(e) · ID {{ venue.id }}</span>
|
||||
<button class="button" type="submit">Ort speichern</button>
|
||||
</div>
|
||||
</form>
|
||||
{% if venues|length > 1 %}
|
||||
<form class="merge-form" method="post" action="/admin/venues/{{ venue.id }}/merge" onsubmit="return confirm('Dieser Ort wird gelöscht und alle Konzerte werden dem Zielort zugeordnet. Fortfahren?');">
|
||||
<select name="target_venue_id" required aria-label="Zielort">
|
||||
<option value="">Mit anderem Ort zusammenführen …</option>
|
||||
{% for target in venues %}
|
||||
{% if target.id != venue.id %}
|
||||
<option value="{{ target.id }}">{{ target.name }}{% if target.city %}, {{ target.city }}{% endif %}</option>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</select>
|
||||
<button class="delete-button" type="submit">Zusammenführen</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% else %}
|
||||
<p>Noch keine Veranstaltungsorte vorhanden.</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,38 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Patch-Bilder · Pingu Concerts</title>
|
||||
<link rel="stylesheet" href="/static/css/style.css">
|
||||
<style>
|
||||
.patch-admin-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 14px; }
|
||||
.patch-admin-card { display: grid; gap: 12px; align-content: start; }
|
||||
.patch-preview { width: 96px; height: 96px; object-fit: contain; padding: 8px; background: #242424; border: 2px dashed #78716c; border-radius: 12px; }
|
||||
.patch-placeholder { display: grid; place-items: center; font-size: 2.5rem; }
|
||||
.patch-upload { display: grid; gap: 8px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header><div class="header-inner"><a href="/" class="logo">🎸 Pingu <span>Concerts</span></a><span>{{ user.display_name }}</span></div></header>
|
||||
<main>
|
||||
<div class="page-title"><h1>🧵 Patch-Bilder</h1><p>Grafiken für die Patches auf der virtuellen Kutte verwalten.</p></div>
|
||||
{% set admin_section = 'patches' %}{% include '_admin_nav.html' %}
|
||||
<section class="admin-section">
|
||||
<div class="patch-admin-grid">
|
||||
{% for patch in patches %}
|
||||
<article class="admin-row patch-admin-card">
|
||||
{% if patch.image_path %}<img class="patch-preview" src="{{ patch.image_path }}" alt="{{ patch.name }}">{% else %}<div class="patch-preview patch-placeholder">{{ patch.icon }}</div>{% endif %}
|
||||
<div><strong>{{ patch.name }}</strong><div class="admin-meta">{{ patch.description }} · {{ patch.code }}</div></div>
|
||||
<form class="patch-upload" method="post" action="/admin/patches/{{ patch.code }}" enctype="multipart/form-data">
|
||||
<input type="file" name="image" accept="image/jpeg,image/png,image/webp" required>
|
||||
<button class="button" type="submit">{% if patch.image_path %}Bild ersetzen{% else %}Bild hochladen{% endif %}</button>
|
||||
</form>
|
||||
{% if patch.image_path %}<form method="post" action="/admin/patches/{{ patch.code }}/delete" onsubmit="return confirm('Patch-Bild entfernen und wieder das Symbol verwenden?');"><button class="admin-danger" type="submit">Bild entfernen</button></form>{% endif %}
|
||||
</article>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,44 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Benutzerverwaltung · Pingu Concerts</title>
|
||||
<link rel="stylesheet" href="/static/css/style.css">
|
||||
<style>
|
||||
.invite-link { display: block; margin-top: 14px; padding: 12px; overflow-wrap: anywhere; background: #0f1420; border: 1px solid var(--border); border-radius: 9px; color: #c4b5fd; }
|
||||
.user-row { display: grid; grid-template-columns: minmax(0, 1fr) auto auto; gap: 12px; align-items: center; }
|
||||
.user-row form { margin: 0; }
|
||||
.role-form { display: flex; gap: 8px; }
|
||||
@media (max-width: 700px) { .user-row { grid-template-columns: 1fr; } }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header><div class="header-inner"><a href="/" class="logo">🎸 Pingu <span>Concerts</span></a><span>{{ user.display_name }}</span></div></header>
|
||||
<main>
|
||||
<div class="page-title"><h1>⚙️ Verwaltung</h1><p>Benutzerkonten und Einladungen verwalten.</p></div>
|
||||
{% set admin_section = 'users' %}{% include '_admin_nav.html' %}
|
||||
<section class="admin-section">
|
||||
<h2>Einladung erstellen</h2>
|
||||
<p>Der Link kann einmalig verwendet werden.</p>
|
||||
<form method="post" action="/admin/invites"><button class="button" type="submit">+ Einladungslink erzeugen</button></form>
|
||||
{% if invite_url %}<label class="invite-link">{{ invite_url }}</label>{% endif %}
|
||||
</section>
|
||||
<section class="admin-section">
|
||||
<h2>Benutzer</h2>
|
||||
<div class="admin-list">
|
||||
{% for managed_user in users %}
|
||||
<div class="admin-row user-row">
|
||||
<div><strong>{{ managed_user.display_name }}</strong><div class="admin-meta">@{{ managed_user.username }} · {{ managed_user.email }} · seit {{ managed_user.created_at }}</div></div>
|
||||
<form class="role-form" method="post" action="/admin/users/{{ managed_user.id }}">
|
||||
<select name="role" aria-label="Rolle für {{ managed_user.username }}"><option value="user" {% if not managed_user.is_admin %}selected{% endif %}>Benutzer</option><option value="admin" {% if managed_user.is_admin %}selected{% endif %}>Admin</option></select>
|
||||
<button class="button" type="submit">Speichern</button>
|
||||
</form>
|
||||
{% if managed_user.id != user.id %}<form method="post" action="/admin/users/{{ managed_user.id }}/delete" onsubmit="return confirm('Diesen Benutzer wirklich löschen?');"><button class="admin-danger" type="submit">Löschen</button></form>{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,59 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Veranstaltungsorte · Pingu Concerts</title>
|
||||
<link rel="stylesheet" href="/static/css/style.css">
|
||||
<style>
|
||||
.venue-fields { display: grid; grid-template-columns: 2fr 2fr .8fr 1.4fr 1.2fr; gap: 8px; }
|
||||
.venue-fields input, .merge-form select { min-width: 0; width: 100%; }
|
||||
.venue-aliases { margin-top: 8px; }
|
||||
.venue-actions { margin-top: 10px; justify-content: space-between; }
|
||||
.venue-actions label { display: flex; align-items: center; gap: 6px; }
|
||||
.merge-form { display: grid; grid-template-columns: minmax(180px, 1fr) auto; gap: 8px; margin-top: 12px; padding-top: 12px; border-top: 1px solid var(--border); }
|
||||
.delete-form { margin-top: 10px; text-align: right; }
|
||||
@media (max-width: 900px) { .venue-fields { grid-template-columns: 1fr 1fr; } }
|
||||
@media (max-width: 560px) { .venue-fields, .merge-form { grid-template-columns: 1fr; } }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header><div class="header-inner"><a href="/" class="logo">🎸 Pingu <span>Concerts</span></a><span>{{ user.display_name }}</span></div></header>
|
||||
<main>
|
||||
<div class="page-title"><h1>📍 Veranstaltungsorte</h1><p>Orte korrigieren, bestätigen, zusammenführen oder entfernen.</p></div>
|
||||
{% set admin_section = 'venues' %}{% include '_admin_nav.html' %}
|
||||
<section class="admin-section">
|
||||
<div class="admin-list">
|
||||
{% for venue in venues %}
|
||||
<article class="admin-row">
|
||||
<form method="post" action="/admin/venues/{{ venue.id }}">
|
||||
<div class="venue-fields">
|
||||
<input name="name" value="{{ venue.name }}" aria-label="Name" required>
|
||||
<input name="street" value="{{ venue.street }}" placeholder="Straße" aria-label="Straße">
|
||||
<input name="postal_code" value="{{ venue.postal_code }}" placeholder="PLZ" aria-label="PLZ">
|
||||
<input name="city" value="{{ venue.city }}" placeholder="Ort" aria-label="Ort">
|
||||
<input name="country" value="{{ venue.country }}" placeholder="Land" aria-label="Land">
|
||||
</div>
|
||||
<input class="venue-aliases" name="aliases" value="{{ venue.aliases }}" placeholder="Aliase, durch Komma getrennt" aria-label="Aliase">
|
||||
<div class="admin-actions venue-actions">
|
||||
<label><input type="checkbox" name="is_verified" {% if venue.is_verified %}checked{% endif %}> bestätigt</label>
|
||||
<span class="admin-meta">Quelle: {{ venue.source }} · {{ venue.concert_count }} Konzert(e) · ID {{ venue.id }}</span>
|
||||
<button class="button" type="submit">Speichern</button>
|
||||
</div>
|
||||
</form>
|
||||
{% if venues|length > 1 %}
|
||||
<form class="merge-form" method="post" action="/admin/venues/{{ venue.id }}/merge" onsubmit="return confirm('Alle Konzerte werden dem Zielort zugeordnet und dieser Eintrag wird gelöscht. Fortfahren?');">
|
||||
<select name="target_venue_id" required><option value="">Mit anderem Ort zusammenführen …</option>{% for target in venues %}{% if target.id != venue.id %}<option value="{{ target.id }}">{{ target.name }}{% if target.city %}, {{ target.city }}{% endif %}</option>{% endif %}{% endfor %}</select>
|
||||
<button class="admin-danger" type="submit">Zusammenführen</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
<form class="delete-form" method="post" action="/admin/venues/{{ venue.id }}/delete" onsubmit="return confirm('{% if venue.concert_count %}Der Ort ist mit {{ venue.concert_count }} Konzert(en) verknüpft. Diese Konzerte haben danach keinen Veranstaltungsort mehr. {% endif %}Ort endgültig löschen?');">
|
||||
<button class="admin-danger" type="submit">Ort löschen{% if venue.concert_count %} ({{ venue.concert_count }} Verknüpfung(en)){% endif %}</button>
|
||||
</form>
|
||||
</article>
|
||||
{% else %}<p>Noch keine Veranstaltungsorte vorhanden.</p>{% endfor %}
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -135,6 +135,7 @@
|
||||
Veranstaltungsort
|
||||
</label>
|
||||
|
||||
<div class="venue-search-control">
|
||||
<input
|
||||
type="text"
|
||||
id="venue-search"
|
||||
@@ -148,6 +149,7 @@
|
||||
id="venue-results"
|
||||
class="venue-results"
|
||||
></div>
|
||||
</div>
|
||||
|
||||
|
||||
<div
|
||||
|
||||
Reference in New Issue
Block a user