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)
|
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,
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
Reference in New Issue
Block a user