mirror of
https://github.com/skoelle/icloud-contacts-sync.git
synced 2026-09-17 15:30:24 +00:00
web dashboard special links
This commit is contained in:
@@ -283,6 +283,51 @@ def admin_toggle(
|
||||
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)
|
||||
def web_search(
|
||||
request: Request,
|
||||
|
||||
@@ -242,6 +242,44 @@
|
||||
.search-form button:hover {
|
||||
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>
|
||||
</head>
|
||||
<body>
|
||||
@@ -263,9 +301,24 @@
|
||||
<div class="dashboard-grid">
|
||||
<div class="box">
|
||||
<div class="box-title">Kontakte</div>
|
||||
<div class="contact-stats">
|
||||
<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 class="box">
|
||||
<div class="box-title">Letzter Sync</div>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<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>
|
||||
* {
|
||||
margin: 0;
|
||||
@@ -176,7 +176,7 @@
|
||||
<div class="meta">
|
||||
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 %}
|
||||
· {{ contacts|length }} Kontakte (max. 200 angezeigt)
|
||||
· {% if search_title %}{{ search_title }}: {% endif %}{{ contacts|length }} Kontakte (max. 200 angezeigt)
|
||||
</div>
|
||||
|
||||
<form class="search-form" method="get" action="/search">
|
||||
|
||||
@@ -154,6 +154,60 @@ def get_contact_count(conn, account: str | None) -> int:
|
||||
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]:
|
||||
where_clause, params = _account_filter_clause(account)
|
||||
today = date.today()
|
||||
|
||||
Reference in New Issue
Block a user