web dashboard special links

This commit is contained in:
2026-08-05 22:35:34 +02:00
parent 3f5a8dff91
commit ddb65055d8
4 changed files with 157 additions and 5 deletions
+45
View File
@@ -283,6 +283,51 @@ def admin_toggle(
return RedirectResponse(url="/admin", status_code=303) return RedirectResponse(url="/admin", status_code=303)
@app.get("/search/special", response_class=HTMLResponse)
def web_search_special(
request: Request,
type: str = Query(...),
current_user: str = Depends(get_current_user),
):
account_name, is_admin, show_all = _resolve_effective_account(request, current_user)
query_fn = {
"no_photo": db.search_contacts_without_photo,
"no_city": db.search_contacts_without_city,
"no_social": db.search_contacts_without_social,
}.get(type)
if not query_fn:
return RedirectResponse(url="/", status_code=303)
title_map = {
"no_photo": "Kontakte ohne Bild",
"no_city": "Kontakte ohne Stadt",
"no_social": "Kontakte ohne Social Profil",
}
with db.get_connection() as conn:
rows = query_fn(conn, account_name)
for row in rows:
if not row.get("full_name"):
row["full_name"] = db._build_full_name(row)
return templates.TemplateResponse(
"index.html",
{
"request": request,
"current_user": current_user,
"is_admin": is_admin,
"show_all": show_all,
"account_name": account_name or "alle Accounts",
"contacts": rows,
"search": "",
"search_title": title_map.get(type, "Suche"),
},
)
@app.get("/search", response_class=HTMLResponse) @app.get("/search", response_class=HTMLResponse)
def web_search( def web_search(
request: Request, request: Request,
+55 -2
View File
@@ -242,6 +242,44 @@
.search-form button:hover { .search-form button:hover {
background: #0055aa; background: #0055aa;
} }
.contact-stats {
display: flex;
gap: 1.5rem;
}
.contact-stats-left {
flex: 1;
}
.contact-stats-right {
flex: 1;
display: flex;
flex-direction: column;
gap: 0.5rem;
justify-content: center;
}
.special-search {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.4rem 0.6rem;
background: #f8f9fa;
border-radius: 4px;
text-decoration: none;
color: inherit;
font-size: 0.8125rem;
transition: background 0.15s;
}
.special-search:hover {
background: #e9ecef;
}
.special-search-label {
color: #555;
}
</style> </style>
</head> </head>
<body> <body>
@@ -263,8 +301,23 @@
<div class="dashboard-grid"> <div class="dashboard-grid">
<div class="box"> <div class="box">
<div class="box-title">Kontakte</div> <div class="box-title">Kontakte</div>
<div class="stat-value">{{ contact_count }}</div> <div class="contact-stats">
<div class="stat-label">gespeicherte Kontakte</div> <div class="contact-stats-left">
<div class="stat-value">{{ contact_count }}</div>
<div class="stat-label">gespeicherte Kontakte</div>
</div>
<div class="contact-stats-right">
<a href="/search/special?type=no_photo" class="special-search">
<span class="special-search-label">ohne Bild</span>
</a>
<a href="/search/special?type=no_city" class="special-search">
<span class="special-search-label">ohne City</span>
</a>
<a href="/search/special?type=no_social" class="special-search">
<span class="special-search-label">ohne Social</span>
</a>
</div>
</div>
</div> </div>
<div class="box"> <div class="box">
+3 -3
View File
@@ -6,7 +6,7 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg"> <link rel="icon" type="image/svg+xml" href="/static/favicon.svg">
<title>Kontakte {{ account_name }}</title> <title>{% if search_title %}{{ search_title }} {% endif %}{{ account_name }}</title>
<style> <style>
* { * {
margin: 0; margin: 0;
@@ -175,8 +175,8 @@
<h1><a href="/" style="text-decoration:none;color:inherit;display:inline-flex;align-items:center;gap:0.4rem"><img src="/static/favicon.svg" alt="" style="height:1.5em;width:1.5em;vertical-align:middle">Kontakte</a></h1> <h1><a href="/" style="text-decoration:none;color:inherit;display:inline-flex;align-items:center;gap:0.4rem"><img src="/static/favicon.svg" alt="" style="height:1.5em;width:1.5em;vertical-align:middle">Kontakte</a></h1>
<div class="meta"> <div class="meta">
Angemeldet als <strong>{{ current_user }}</strong> Angemeldet als <strong>{{ current_user }}</strong>
{% if is_admin %}(<a href="/admin" style="color:inherit">Admin</a>{% if show_all %}, sieht alle Accounts{% endif %}){% else %}(Account: {{ account_name }}){% endif %} {% if is_admin %}(<a href="/admin" style="color:inherit">Admin</a> {% if show_all %}, sieht alle Accounts{% endif %}){% else %}(Account: {{ account_name }}){% endif %}
· {{ contacts|length }} Kontakte (max. 200 angezeigt) · {% if search_title %}{{ search_title }}: {% endif %}{{ contacts|length }} Kontakte (max. 200 angezeigt)
</div> </div>
<form class="search-form" method="get" action="/search"> <form class="search-form" method="get" action="/search">
+54
View File
@@ -154,6 +154,60 @@ def get_contact_count(conn, account: str | None) -> int:
return cur.fetchone()["total"] return cur.fetchone()["total"]
def search_contacts_without_photo(conn, account: str | None) -> list[dict]:
where_clause, params = _account_filter_clause(account)
name_cond = "AND given_name IS NOT NULL AND given_name != '' AND family_name IS NOT NULL AND family_name != ''"
city_cond = "AND (JSON_SEARCH(addresses, 'one', '%', NULL, '$[*].city') IS NOT NULL)"
with conn.cursor() as cur:
cur.execute(
f"""SELECT id, full_name, given_name, middle_name, family_name,
prefix, suffix, organization, birthday, account, photo_url
FROM contacts {where_clause}
AND photo_base64 IS NULL AND photo_url IS NULL
{name_cond} {city_cond}
AND family_name != 'X'
ORDER BY full_name""",
params,
)
return cur.fetchall()
def search_contacts_without_city(conn, account: str | None) -> list[dict]:
where_clause, params = _account_filter_clause(account)
name_cond = "AND given_name IS NOT NULL AND given_name != '' AND family_name IS NOT NULL AND family_name != ''"
contact_cond = "AND (JSON_LENGTH(phones) > 0 OR JSON_LENGTH(emails) > 0)"
city_cond = "AND (JSON_LENGTH(addresses) = 0 OR JSON_SEARCH(addresses, 'one', '%', NULL, '$[*].city') IS NULL)"
with conn.cursor() as cur:
cur.execute(
f"""SELECT id, full_name, given_name, middle_name, family_name,
prefix, suffix, organization, birthday, account, photo_url
FROM contacts {where_clause}
{name_cond} {contact_cond} {city_cond}
AND family_name != 'X'
ORDER BY full_name""",
params,
)
return cur.fetchall()
def search_contacts_without_social(conn, account: str | None) -> list[dict]:
where_clause, params = _account_filter_clause(account)
name_cond = "AND given_name IS NOT NULL AND given_name != '' AND family_name IS NOT NULL AND family_name != ''"
contact_cond = "AND (JSON_LENGTH(phones) > 0 OR JSON_LENGTH(emails) > 0)"
social_cond = "AND (social_profiles IS NULL OR JSON_LENGTH(social_profiles) = 0)"
with conn.cursor() as cur:
cur.execute(
f"""SELECT id, full_name, given_name, middle_name, family_name,
prefix, suffix, organization, birthday, account, photo_url
FROM contacts {where_clause}
{name_cond} {contact_cond} {social_cond}
AND family_name != 'X'
ORDER BY full_name""",
params,
)
return cur.fetchall()
def get_upcoming_birthdays(conn, account: str | None, days: int = 7) -> list[dict]: def get_upcoming_birthdays(conn, account: str | None, days: int = 7) -> list[dict]:
where_clause, params = _account_filter_clause(account) where_clause, params = _account_filter_clause(account)
today = date.today() today = date.today()