Sprint 12: UI-Vereinheitlichung + Läufigkeits-Tracker

- by-tabs/by-tab: einheitliche Tab/Pill-Navigation in allen Seiten
- by-section-label, by-toolbar: einheitliche Section-Labels und Toolbars
- Design-Tokens: fehlende --c-amber, --c-primary-soft ergänzt, Fallback-Werte entfernt
- sitting.js: sitting-layout für konsistentes flush-Layout (wie walks)
- Läufigkeits-Tracker: neuer Health-Tab für Hündinnen mit Zyklusvorhersage,
  Timeline vergangener Läufigkeiten, Erinnerungen und auto-berechnetem Nächst-Datum
- emptyState-Bug: icon-Parameter muss SVG sein, nicht Icon-Name (dog/bell/warning gefixt)
- SW-Cache: by-v103, APP_VER: 79
This commit is contained in:
rene 2026-04-16 22:31:33 +02:00
parent 32d630d5a1
commit b58789373c
30 changed files with 4344 additions and 523 deletions

View file

@ -8,22 +8,38 @@ router = APIRouter()
logger = logging.getLogger(__name__)
def _dogs_subquery():
"""JSON-Array der Hunde eines Users als Subquery."""
return """(
SELECT json_group_array(json_object(
'id', d.id,
'name', d.name,
'rasse', d.rasse,
'foto_url',d.foto_url
))
FROM dogs d WHERE d.user_id = u.id
)"""
@router.get("/")
async def list_friends(user=Depends(get_current_user)):
uid = user["id"]
dogs_sq = _dogs_subquery()
with db() as conn:
friends = conn.execute("""
friends = conn.execute(f"""
SELECT f.id, f.status, f.created_at,
CASE WHEN f.requester_id=? THEN f.addressee_id ELSE f.requester_id END AS friend_id,
u.name AS friend_name
u.name AS friend_name,
{dogs_sq} AS dogs_json
FROM friendships f
JOIN users u ON u.id = CASE WHEN f.requester_id=? THEN f.addressee_id ELSE f.requester_id END
WHERE (f.requester_id=? OR f.addressee_id=?) AND f.status='accepted'
ORDER BY u.name
""", (uid, uid, uid, uid)).fetchall()
incoming = conn.execute("""
SELECT f.id, f.created_at, u.name AS requester_name, u.id AS requester_id
incoming = conn.execute(f"""
SELECT f.id, f.created_at, u.name AS requester_name, u.id AS requester_id,
{dogs_sq} AS dogs_json
FROM friendships f
JOIN users u ON u.id=f.requester_id
WHERE f.addressee_id=? AND f.status='pending'
@ -38,9 +54,26 @@ async def list_friends(user=Depends(get_current_user)):
ORDER BY f.created_at DESC
""", (uid,)).fetchall()
import json
def _parse(rows):
result = []
for r in rows:
d = dict(r)
if d.get("dogs_json"):
try:
d["dogs"] = json.loads(d["dogs_json"])
except Exception:
d["dogs"] = []
else:
d["dogs"] = []
d.pop("dogs_json", None)
result.append(d)
return result
return {
"friends": [dict(r) for r in friends],
"incoming": [dict(r) for r in incoming],
"friends": _parse(friends),
"incoming": _parse(incoming),
"outgoing": [dict(r) for r in outgoing],
}
@ -50,9 +83,12 @@ async def search_users(q: str = "", user=Depends(get_current_user)):
if len(q.strip()) < 2:
return []
uid = user["id"]
import json
with db() as conn:
rows = conn.execute("""
SELECT u.id, u.name
SELECT u.id, u.name,
(SELECT json_group_array(json_object('name', d.name, 'rasse', d.rasse))
FROM dogs d WHERE d.user_id=u.id AND d.is_public=1) AS dogs_json
FROM users u
WHERE u.id != ?
AND u.name LIKE ?
@ -63,7 +99,17 @@ async def search_users(q: str = "", user=Depends(get_current_user)):
)
LIMIT 20
""", (uid, f"%{q.strip()}%", uid, uid)).fetchall()
return [dict(r) for r in rows]
result = []
for r in rows:
d = dict(r)
try:
d["dogs"] = json.loads(d["dogs_json"]) if d.get("dogs_json") else []
except Exception:
d["dogs"] = []
d.pop("dogs_json", None)
result.append(d)
return result
@router.post("/request/{target_id}", status_code=201)