diff --git a/backend/database.py b/backend/database.py index 5c5516a..3d9757f 100644 --- a/backend/database.py +++ b/backend/database.py @@ -2265,82 +2265,6 @@ def _migrate(conn_factory): except Exception as e: logger.warning(f"Migration behavior_log: {e}") - try: - conn.execute(""" - CREATE TABLE IF NOT EXISTS laeufi_log ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - hund_id INTEGER NOT NULL REFERENCES zucht_hunde(id) ON DELETE CASCADE, - beginn TEXT NOT NULL, - ende TEXT, - notiz TEXT, - created_at TEXT NOT NULL DEFAULT (datetime('now')) - ) - """) - conn.execute(""" - CREATE TABLE IF NOT EXISTS progesteron_tests ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - laeufi_id INTEGER NOT NULL REFERENCES laeufi_log(id) ON DELETE CASCADE, - datum TEXT NOT NULL, - wert REAL, - einheit TEXT NOT NULL DEFAULT 'ng/ml', - labor TEXT, - notiz TEXT, - created_at TEXT NOT NULL DEFAULT (datetime('now')) - ) - """) - conn.execute(""" - CREATE TABLE IF NOT EXISTS deckdaten ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - hund_id INTEGER NOT NULL REFERENCES zucht_hunde(id) ON DELETE CASCADE, - laeufi_id INTEGER REFERENCES laeufi_log(id) ON DELETE SET NULL, - deckdatum TEXT NOT NULL, - ruede_id INTEGER REFERENCES zucht_hunde(id) ON DELETE SET NULL, - ruede_name TEXT, - deckart TEXT NOT NULL DEFAULT 'natuerlich', - traechtig INTEGER NOT NULL DEFAULT 0, - ultraschall_datum TEXT, - notiz TEXT, - created_at TEXT NOT NULL DEFAULT (datetime('now')) - ) - """) - conn.execute("CREATE INDEX IF NOT EXISTS idx_laeufi_hund ON laeufi_log(hund_id, beginn DESC)") - conn.execute("CREATE INDEX IF NOT EXISTS idx_prog_laeufi ON progesteron_tests(laeufi_id, datum)") - conn.execute("CREATE INDEX IF NOT EXISTS idx_deck_hund ON deckdaten(hund_id, deckdatum DESC)") - logger.info("Migration: laeufi_log, progesteron_tests, deckdaten bereit.") - except Exception as e: - logger.warning(f"Migration laeufi: {e}") - - try: - conn.execute(""" - CREATE TABLE IF NOT EXISTS litter_waitlist ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - litter_id INTEGER NOT NULL REFERENCES litters(id) ON DELETE CASCADE, - name TEXT NOT NULL, - email TEXT, - telefon TEXT, - nachricht TEXT, - wunsch_geschlecht TEXT DEFAULT 'egal', - wunsch_farbe TEXT, - prioritaet INTEGER DEFAULT 0, - status TEXT DEFAULT 'anfrage', - notiz TEXT, - created_at TEXT NOT NULL DEFAULT (datetime('now')) - ) - """) - conn.execute("CREATE INDEX IF NOT EXISTS idx_waitlist_litter ON litter_waitlist(litter_id, prioritaet)") - logger.info("Migration: litter_waitlist bereit.") - except Exception as e: - logger.warning(f"Migration litter_waitlist: {e}") - - try: - conn.execute("ALTER TABLE litters ADD COLUMN wurf_rang TEXT") - except Exception: - pass - try: - conn.execute("ALTER TABLE litters ADD COLUMN wurf_name TEXT") - except Exception: - pass - # route_dogs: bestehende Routen allen Hunden des Users zuweisen try: existing = conn.execute("SELECT COUNT(*) FROM route_dogs").fetchone()[0] diff --git a/backend/main.py b/backend/main.py index 4bcd9ba..456dc43 100644 --- a/backend/main.py +++ b/backend/main.py @@ -156,15 +156,17 @@ app.add_middleware(_AppVersionMiddleware) class _CacheControlMiddleware(BaseHTTPMiddleware): """Setzt Cache-Control-Header für statische Assets. - JS/CSS: immer no-cache — SW übernimmt Caching. Immutable wäre gefährlich, - weil Browser-HTTP-Cache nach force-update nicht geleert wird und veraltete - app.js mit falschem APP_VER eine Update-Dauerschleife verursacht. + CSS/JS: no-cache (ETag-Validierung) — iOS cached sonst ewig ohne Ablaufdatum. + Versioned Assets (?v=…): immutable — URL ändert sich bei Updates. """ async def dispatch(self, request: Request, call_next): response = await call_next(request) path = request.url.path if path.startswith(("/css/", "/js/", "/icons/phosphor.svg")): - response.headers["Cache-Control"] = "no-cache, must-revalidate" + if "v=" in str(request.url.query): + response.headers["Cache-Control"] = "public, max-age=31536000, immutable" + else: + response.headers["Cache-Control"] = "no-cache, must-revalidate" return response app.add_middleware(_CacheControlMiddleware) @@ -233,7 +235,6 @@ from routes.moderation import router as moderation_router from routes.notes import router as notes_router from routes.breeder import router as breeder_router from routes.litters import router as litters_router -from routes.laeufi import router as laeufi_router from routes.breeder_photos import router as breeder_photos_router from routes.zucht_hunde import router as zucht_hunde_router from routes.breeder_export import router as breeder_export_router @@ -280,7 +281,6 @@ app.include_router(chat_router, prefix="/api/chat", tags=["Chat"]) app.include_router(admin_router, prefix="/api/admin", tags=["Admin"]) app.include_router(breeder_router, prefix="/api", tags=["Züchter"]) app.include_router(litters_router, prefix="/api", tags=["Würfe"]) -app.include_router(laeufi_router, prefix="/api", tags=["Läufigkeit"]) app.include_router(breeder_photos_router, prefix="/api", tags=["Züchter-Fotos"]) app.include_router(zucht_hunde_router, prefix="/api", tags=["Zuchtkartei"]) app.include_router(breeder_export_router, prefix="/api", tags=["Export"]) @@ -406,7 +406,7 @@ async def serve_media(path: str, request: _Request): raise _HE(404, "Nicht gefunden.") return _media_response(filepath) -APP_VER = "918" # muss mit APP_VER in app.js übereinstimmen +APP_VER = "885" # muss mit APP_VER in app.js übereinstimmen @app.get("/.well-known/assetlinks.json") async def assetlinks(): @@ -523,11 +523,6 @@ async def info_page(): return FileResponse(f"{STATIC_DIR}/landing.html", headers={"Cache-Control": "max-age=3600"}) -@app.get("/zuechter") -async def zuechter_landing(): - return FileResponse(f"{STATIC_DIR}/zuechter.html", headers={"Cache-Control": "max-age=3600"}) - - # ------------------------------------------------------------------ # SEO: Server-gerenderete Wiki-Rassen-Übersicht /wiki/rassen # ------------------------------------------------------------------ diff --git a/backend/routes/breeder.py b/backend/routes/breeder.py index 1afe535..355a575 100644 --- a/backend/routes/breeder.py +++ b/backend/routes/breeder.py @@ -54,25 +54,15 @@ async def breeder_status(user=Depends(get_current_user)): profile = None if row["rolle"] in ("breeder", "admin"): profile = conn.execute( - "SELECT id, zwingername, rasse_text, verein, vdh_mitglied, stadt, website, beschreibung, verified_at " + "SELECT zwingername, rasse_text, verein, vdh_mitglied, stadt, website, beschreibung, verified_at " "FROM breeder_profiles WHERE user_id=?", (user["id"],) ).fetchone() - if profile: - logo = conn.execute( - """SELECT file_path FROM breeder_photos - WHERE breeder_id=? AND entity_type='breeder' - ORDER BY is_primary DESC, id LIMIT 1""", - (profile["id"],) - ).fetchone() - result = { + return { "rolle": row["rolle"], "breeder_status": row["breeder_status"], "profile": dict(profile) if profile else None, } - if profile: - result["profile"]["logo_url"] = f"/media/{logo['file_path']}" if logo else None - return result # ------------------------------------------------------------------ @@ -311,7 +301,7 @@ async def admin_reject_breeder(user_id: int, body: RejectBody, admin=Depends(req # ------------------------------------------------------------------ -# GET /api/breeder/profil/{zwingername} — öffentliches Profil (angereichert) +# GET /api/breeder/profil/{zwingername} — öffentliches Profil # ------------------------------------------------------------------ @router.get("/breeder/profil/{zwingername}") async def breeder_public_profile(zwingername: str): @@ -325,114 +315,12 @@ async def breeder_public_profile(zwingername: str): FROM breeder_profiles bp JOIN users u ON u.id = bp.user_id WHERE LOWER(bp.zwingername) = LOWER(?) - AND u.rolle IN ('breeder', 'admin') - AND (u.breeder_status = 'approved' OR u.rolle = 'admin') + AND u.rolle = 'breeder' + AND u.breeder_status = 'approved' """, (zwingername,)).fetchone() - if not row: - raise HTTPException(404, "Züchter nicht gefunden.") - - breeder_id = row["id"] - result = dict(row) - - # Öffentliche Zuchthunde + ihre wichtigsten Gesundheitstests + Titel - hunde_rows = conn.execute(""" - SELECT id, name, rufname, geschlecht, geburtsdatum, farbe, zuchtbuchnummer, foto_url - FROM zucht_hunde - WHERE breeder_id=? AND is_public=1 AND (sterbedatum IS NULL OR sterbedatum='') - ORDER BY geschlecht, name - """, (breeder_id,)).fetchall() - - hunde = [] - for h in hunde_rows: - hund = dict(h) - # Gesundheitstests (nur öffentliche, nur HD/ED/Augen/Herz) - tests = conn.execute(""" - SELECT test_typ, ergebnis, test_name, untersuch_am - FROM dog_health_tests - WHERE hund_id=? AND is_public=1 - AND test_typ IN ('HD','ED','augen','herz','OCD','patella','ZTP') - ORDER BY test_typ, untersuch_am DESC - """, (h["id"],)).fetchall() - seen = set() - hund["health_tests"] = [] - for t in tests: - if t["test_typ"] not in seen: - seen.add(t["test_typ"]) - hund["health_tests"].append(dict(t)) - # Gentests (nur öffentliche, Zusammenfassung) - gentests = conn.execute(""" - SELECT COUNT(*) as total, - SUM(CASE WHEN ergebnis_klasse='clear' THEN 1 ELSE 0 END) as clear_cnt - FROM dog_genetic_tests WHERE hund_id=? AND is_public=1 - """, (h["id"],)).fetchone() - hund["gentests_total"] = gentests["total"] or 0 - hund["gentests_clear"] = gentests["clear_cnt"] or 0 - # Auszeichnungen (nur Zucht/Champion) - titles = conn.execute(""" - SELECT titel_name FROM dog_titles - WHERE hund_id=? AND titel_typ IN ('champion','zucht','ausstellung') - ORDER BY verliehen_am DESC LIMIT 3 - """, (h["id"],)).fetchall() - hund["titel"] = [t["titel_name"] for t in titles] - hunde.append(hund) - - result["hunde"] = hunde - - # Sichtbare Würfe - wuerfe = conn.execute(""" - SELECT id, vater_name, mutter_name, geburt_datum, erwartetes_datum, - status, welpen_gesamt, welpen_verfuegbar, preis_spanne, beschreibung - FROM litters - WHERE breeder_id=? AND sichtbar=1 AND status != 'abgeschlossen' - ORDER BY COALESCE(geburt_datum, erwartetes_datum) DESC - """, (breeder_id,)).fetchall() - result["wuerfe"] = [dict(w) for w in wuerfe] - - # Gesundheits-Statistik (aggregiert über alle öffentlichen Hunde) - hd_stats = conn.execute(""" - SELECT ergebnis, COUNT(*) as cnt FROM dog_health_tests - WHERE hund_id IN (SELECT id FROM zucht_hunde WHERE breeder_id=? AND is_public=1) - AND test_typ='HD' AND is_public=1 - GROUP BY ergebnis - """, (breeder_id,)).fetchall() - result["hd_stats"] = [dict(r) for r in hd_stats] - - ed_stats = conn.execute(""" - SELECT ergebnis, COUNT(*) as cnt FROM dog_health_tests - WHERE hund_id IN (SELECT id FROM zucht_hunde WHERE breeder_id=? AND is_public=1) - AND test_typ='ED' AND is_public=1 - GROUP BY ergebnis - """, (breeder_id,)).fetchall() - result["ed_stats"] = [dict(r) for r in ed_stats] - - # Logo = primäres Bild der entity_type='breeder' Fotos - logo = conn.execute(""" - SELECT file_path FROM breeder_photos - WHERE breeder_id=? AND entity_type='breeder' AND is_primary=1 - LIMIT 1 - """, (breeder_id,)).fetchone() - if not logo: - logo = conn.execute(""" - SELECT file_path FROM breeder_photos - WHERE breeder_id=? AND entity_type='breeder' - ORDER BY sort_order, id LIMIT 1 - """, (breeder_id,)).fetchone() - result["logo_url"] = f"/media/{logo['file_path']}" if logo else None - - # Öffentliche Fotos für die Gallery (alle entity_type='breeder', max. 12) - photos = conn.execute(""" - SELECT file_path, thumbnail_path, caption, is_primary FROM breeder_photos - WHERE breeder_id=? AND entity_type='breeder' AND visibility IN ('public','inquiry') - ORDER BY is_primary DESC, sort_order, id LIMIT 12 - """, (breeder_id,)).fetchall() - result["fotos"] = [{ - "url": f"/media/{p['file_path']}", - "thumb": f"/media/{p['thumbnail_path']}" if p['thumbnail_path'] else f"/media/{p['file_path']}", - "caption": p["caption"] or "", - "primary": bool(p["is_primary"]), - } for p in photos] - - return result + if not row: + raise HTTPException(404, "Züchter nicht gefunden.") + return dict(row) # ------------------------------------------------------------------ diff --git a/backend/routes/laeufi.py b/backend/routes/laeufi.py deleted file mode 100644 index 22189bd..0000000 --- a/backend/routes/laeufi.py +++ /dev/null @@ -1,307 +0,0 @@ -"""BAN YARO — Läufigkeit, Progesterontests & Trächtigkeit (Züchter)""" - -from fastapi import APIRouter, Depends, HTTPException -from pydantic import BaseModel -from typing import Optional -from datetime import date, timedelta - -from database import db -from auth import get_current_user - -router = APIRouter() - - -def _require_breeder(user=Depends(get_current_user)): - if user["rolle"] not in ("breeder", "admin"): - raise HTTPException(403, "Nur für verifizierte Züchter.") - return user - - -def _check_hund_owner(hund_id: int, user: dict, conn): - row = conn.execute( - """SELECT zh.id, bp.user_id AS owner_user_id FROM zucht_hunde zh - JOIN breeder_profiles bp ON bp.id = zh.breeder_id - WHERE zh.id=?""", (hund_id,) - ).fetchone() - if not row: - raise HTTPException(404, "Hund nicht gefunden.") - if user["rolle"] != "admin" and row["owner_user_id"] != user["id"]: - raise HTTPException(403, "Kein Zugriff.") - return row - - -def _check_laeufi_owner(laeufi_id: int, user: dict, conn): - row = conn.execute( - """SELECT l.id, bp.user_id AS owner_user_id FROM laeufi_log l - JOIN zucht_hunde zh ON zh.id = l.hund_id - JOIN breeder_profiles bp ON bp.id = zh.breeder_id - WHERE l.id=?""", (laeufi_id,) - ).fetchone() - if not row: - raise HTTPException(404, "Läufigkeit nicht gefunden.") - if user["rolle"] != "admin" and row["owner_user_id"] != user["id"]: - raise HTTPException(403, "Kein Zugriff.") - return row - - -# ------------------------------------------------------------------ -# Meilensteine berechnen -# ------------------------------------------------------------------ -_MEILENSTEINE = [ - (21, "Frühester Ultraschall möglich"), - (25, "Ultraschall empfohlen (Welpen erkennbar)"), - (35, "Bauch wird sichtbar"), - (45, "Röntgen möglich (Skelettanzahl)"), - (56, "Wurfbox aufstellen"), - (58, "Tägliche Temperaturmessung beginnen"), - (63, "Erwarteter Geburtstermin"), - (65, "Tierarzt konsultieren wenn keine Geburt"), -] - -def _calc_meilensteine(deckdatum_str: str) -> list: - try: - deck = date.fromisoformat(deckdatum_str) - except Exception: - return [] - return [ - { - "tag": tag, - "datum": (deck + timedelta(days=tag)).isoformat(), - "label": label, - "vorbei": (deck + timedelta(days=tag)) < date.today(), - } - for tag, label in _MEILENSTEINE - ] - - -# ------------------------------------------------------------------ -# Schemas -# ------------------------------------------------------------------ -class LaeufiCreate(BaseModel): - beginn: str - ende: Optional[str] = None - notiz: Optional[str] = None - -class LaeufiUpdate(BaseModel): - beginn: Optional[str] = None - ende: Optional[str] = None - notiz: Optional[str] = None - -class ProgestCreate(BaseModel): - datum: str - wert: Optional[float] = None - einheit: str = "ng/ml" - labor: Optional[str] = None - notiz: Optional[str] = None - -class ProgestUpdate(BaseModel): - datum: Optional[str] = None - wert: Optional[float] = None - einheit: Optional[str] = None - labor: Optional[str] = None - notiz: Optional[str] = None - -class DeckCreate(BaseModel): - deckdatum: str - laeufi_id: Optional[int] = None - ruede_id: Optional[int] = None - ruede_name: Optional[str] = None - deckart: str = "natuerlich" - traechtig: int = 0 - ultraschall_datum: Optional[str] = None - notiz: Optional[str] = None - -class DeckUpdate(BaseModel): - deckdatum: Optional[str] = None - ruede_id: Optional[int] = None - ruede_name: Optional[str] = None - deckart: Optional[str] = None - traechtig: Optional[int] = None - ultraschall_datum: Optional[str] = None - notiz: Optional[str] = None - - -# ------------------------------------------------------------------ -# Läufigkeit -# ------------------------------------------------------------------ -@router.get("/laeufi/{hund_id}") -async def list_laeufi(hund_id: int, user=Depends(_require_breeder)): - with db() as conn: - _check_hund_owner(hund_id, user, conn) - rows = conn.execute( - "SELECT * FROM laeufi_log WHERE hund_id=? ORDER BY beginn DESC", - (hund_id,) - ).fetchall() - return [dict(r) for r in rows] - - -@router.post("/laeufi/{hund_id}", status_code=201) -async def add_laeufi(hund_id: int, body: LaeufiCreate, user=Depends(_require_breeder)): - with db() as conn: - _check_hund_owner(hund_id, user, conn) - cur = conn.execute( - "INSERT INTO laeufi_log (hund_id, beginn, ende, notiz) VALUES (?,?,?,?)", - (hund_id, body.beginn, body.ende, body.notiz) - ) - row = conn.execute("SELECT * FROM laeufi_log WHERE id=?", (cur.lastrowid,)).fetchone() - return dict(row) - - -@router.put("/laeufi/entry/{laeufi_id}") -async def update_laeufi(laeufi_id: int, body: LaeufiUpdate, user=Depends(_require_breeder)): - with db() as conn: - _check_laeufi_owner(laeufi_id, user, conn) - fields = {k: v for k, v in body.model_dump().items() if v is not None} - if fields: - sets = ", ".join(f"{k}=?" for k in fields) - conn.execute(f"UPDATE laeufi_log SET {sets} WHERE id=?", (*fields.values(), laeufi_id)) - row = conn.execute("SELECT * FROM laeufi_log WHERE id=?", (laeufi_id,)).fetchone() - return dict(row) - - -@router.delete("/laeufi/entry/{laeufi_id}", status_code=204) -async def delete_laeufi(laeufi_id: int, user=Depends(_require_breeder)): - with db() as conn: - _check_laeufi_owner(laeufi_id, user, conn) - conn.execute("DELETE FROM laeufi_log WHERE id=?", (laeufi_id,)) - - -# ------------------------------------------------------------------ -# Progesterontests -# ------------------------------------------------------------------ -@router.get("/laeufi/entry/{laeufi_id}/prog") -async def list_prog(laeufi_id: int, user=Depends(_require_breeder)): - with db() as conn: - _check_laeufi_owner(laeufi_id, user, conn) - rows = conn.execute( - "SELECT * FROM progesteron_tests WHERE laeufi_id=? ORDER BY datum", - (laeufi_id,) - ).fetchall() - return [dict(r) for r in rows] - - -@router.post("/laeufi/entry/{laeufi_id}/prog", status_code=201) -async def add_prog(laeufi_id: int, body: ProgestCreate, user=Depends(_require_breeder)): - with db() as conn: - _check_laeufi_owner(laeufi_id, user, conn) - cur = conn.execute( - "INSERT INTO progesteron_tests (laeufi_id, datum, wert, einheit, labor, notiz) VALUES (?,?,?,?,?,?)", - (laeufi_id, body.datum, body.wert, body.einheit, body.labor, body.notiz) - ) - row = conn.execute("SELECT * FROM progesteron_tests WHERE id=?", (cur.lastrowid,)).fetchone() - return dict(row) - - -@router.put("/laeufi/prog/{prog_id}") -async def update_prog(prog_id: int, body: ProgestUpdate, user=Depends(_require_breeder)): - with db() as conn: - pt = conn.execute( - """SELECT pt.id, bp.user_id AS owner_user_id FROM progesteron_tests pt - JOIN laeufi_log l ON l.id = pt.laeufi_id - JOIN zucht_hunde zh ON zh.id = l.hund_id - JOIN breeder_profiles bp ON bp.id = zh.breeder_id - WHERE pt.id=?""", (prog_id,) - ).fetchone() - if not pt: - raise HTTPException(404) - if user["rolle"] != "admin" and pt["owner_user_id"] != user["id"]: - raise HTTPException(403) - fields = {k: v for k, v in body.model_dump().items() if v is not None} - if fields: - sets = ", ".join(f"{k}=?" for k in fields) - conn.execute(f"UPDATE progesteron_tests SET {sets} WHERE id=?", (*fields.values(), prog_id)) - row = conn.execute("SELECT * FROM progesteron_tests WHERE id=?", (prog_id,)).fetchone() - return dict(row) - - -@router.delete("/laeufi/prog/{prog_id}", status_code=204) -async def delete_prog(prog_id: int, user=Depends(_require_breeder)): - with db() as conn: - pt = conn.execute( - """SELECT pt.id, bp.user_id AS owner_user_id FROM progesteron_tests pt - JOIN laeufi_log l ON l.id = pt.laeufi_id - JOIN zucht_hunde zh ON zh.id = l.hund_id - JOIN breeder_profiles bp ON bp.id = zh.breeder_id - WHERE pt.id=?""", (prog_id,) - ).fetchone() - if not pt: - raise HTTPException(404) - if user["rolle"] != "admin" and pt["owner_user_id"] != user["id"]: - raise HTTPException(403) - conn.execute("DELETE FROM progesteron_tests WHERE id=?", (prog_id,)) - - -# ------------------------------------------------------------------ -# Deckdaten & Trächtigkeit -# ------------------------------------------------------------------ -@router.get("/laeufi/deck/{hund_id}") -async def list_deck(hund_id: int, user=Depends(_require_breeder)): - with db() as conn: - _check_hund_owner(hund_id, user, conn) - rows = conn.execute( - "SELECT * FROM deckdaten WHERE hund_id=? ORDER BY deckdatum DESC", - (hund_id,) - ).fetchall() - result = [] - for r in rows: - d = dict(r) - d["meilensteine"] = _calc_meilensteine(d["deckdatum"]) - result.append(d) - return result - - -@router.post("/laeufi/deck/{hund_id}", status_code=201) -async def add_deck(hund_id: int, body: DeckCreate, user=Depends(_require_breeder)): - with db() as conn: - _check_hund_owner(hund_id, user, conn) - cur = conn.execute( - """INSERT INTO deckdaten - (hund_id, laeufi_id, deckdatum, ruede_id, ruede_name, deckart, - traechtig, ultraschall_datum, notiz) - VALUES (?,?,?,?,?,?,?,?,?)""", - (hund_id, body.laeufi_id, body.deckdatum, body.ruede_id, body.ruede_name, - body.deckart, body.traechtig, body.ultraschall_datum, body.notiz) - ) - row = conn.execute("SELECT * FROM deckdaten WHERE id=?", (cur.lastrowid,)).fetchone() - d = dict(row) - d["meilensteine"] = _calc_meilensteine(d["deckdatum"]) - return d - - -@router.put("/laeufi/deck/entry/{deck_id}") -async def update_deck(deck_id: int, body: DeckUpdate, user=Depends(_require_breeder)): - with db() as conn: - dk = conn.execute( - """SELECT d.id, bp.user_id AS owner_user_id FROM deckdaten d - JOIN zucht_hunde zh ON zh.id = d.hund_id - JOIN breeder_profiles bp ON bp.id = zh.breeder_id - WHERE d.id=?""", (deck_id,) - ).fetchone() - if not dk: - raise HTTPException(404) - if user["rolle"] != "admin" and dk["owner_user_id"] != user["id"]: - raise HTTPException(403) - fields = {k: v for k, v in body.model_dump().items() if v is not None} - if fields: - sets = ", ".join(f"{k}=?" for k in fields) - conn.execute(f"UPDATE deckdaten SET {sets} WHERE id=?", (*fields.values(), deck_id)) - row = conn.execute("SELECT * FROM deckdaten WHERE id=?", (deck_id,)).fetchone() - d = dict(row) - d["meilensteine"] = _calc_meilensteine(d["deckdatum"]) - return d - - -@router.delete("/laeufi/deck/entry/{deck_id}", status_code=204) -async def delete_deck(deck_id: int, user=Depends(_require_breeder)): - with db() as conn: - dk = conn.execute( - """SELECT d.id, bp.user_id AS owner_user_id FROM deckdaten d - JOIN zucht_hunde zh ON zh.id = d.hund_id - JOIN breeder_profiles bp ON bp.id = zh.breeder_id - WHERE d.id=?""", (deck_id,) - ).fetchone() - if not dk: - raise HTTPException(404) - if user["rolle"] != "admin" and dk["owner_user_id"] != user["id"]: - raise HTTPException(403) - conn.execute("DELETE FROM deckdaten WHERE id=?", (deck_id,)) diff --git a/backend/routes/litters.py b/backend/routes/litters.py index 09250d8..ddc810c 100644 --- a/backend/routes/litters.py +++ b/backend/routes/litters.py @@ -27,27 +27,23 @@ def _require_breeder(user=Depends(get_current_user)): # Schemas # ------------------------------------------------------------------ class LitterCreate(BaseModel): - wurf_rang: Optional[str] = None # A, B, C … - wurf_name: Optional[str] = None # z.B. "Vatertags-Wurf" vater_name: Optional[str] = None mutter_name: Optional[str] = None - vater_id: Optional[int] = None - mutter_id: Optional[int] = None - geburt_datum: Optional[str] = None - erwartetes_datum: Optional[str] = None + vater_id: Optional[int] = None # FK zucht_hunde + mutter_id: Optional[int] = None # FK zucht_hunde + geburt_datum: Optional[str] = None # YYYY-MM-DD + erwartetes_datum: Optional[str] = None # YYYY-MM-DD welpen_gesamt: Optional[int] = None welpen_verfuegbar: Optional[int] = None beschreibung: Optional[str] = None gesundheitstests: Optional[str] = None preis_spanne: Optional[str] = None - status: str = "geplant" + status: str = "geplant" # geplant|geboren|verfuegbar|abgeschlossen sichtbar: int = 0 sichtbar_bis: Optional[str] = None class LitterUpdate(BaseModel): - wurf_rang: Optional[str] = None - wurf_name: Optional[str] = None vater_name: Optional[str] = None mutter_name: Optional[str] = None vater_id: Optional[int] = None @@ -193,16 +189,13 @@ async def create_litter(body: LitterCreate, user=Depends(_require_breeder)): cur = conn.execute( """INSERT INTO litters - (breeder_id, wurf_rang, wurf_name, - vater_name, mutter_name, vater_id, mutter_id, + (breeder_id, vater_name, mutter_name, vater_id, mutter_id, geburt_datum, erwartetes_datum, welpen_gesamt, welpen_verfuegbar, beschreibung, gesundheitstests, preis_spanne, status, sichtbar, sichtbar_bis) - VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""", + VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""", ( profile["id"], - body.wurf_rang, - body.wurf_name, body.vater_name, body.mutter_name, body.vater_id, @@ -657,98 +650,3 @@ async def generate_contract( """ return HTMLResponse(content=html) - - -# ------------------------------------------------------------------ -# Warteliste -# ------------------------------------------------------------------ -class WaitlistEntry(BaseModel): - name: str - email: Optional[str] = None - telefon: Optional[str] = None - nachricht: Optional[str] = None - wunsch_geschlecht: str = "egal" - wunsch_farbe: Optional[str] = None - prioritaet: int = 0 - status: str = "anfrage" - notiz: Optional[str] = None - - -class WaitlistUpdate(BaseModel): - name: Optional[str] = None - email: Optional[str] = None - telefon: Optional[str] = None - nachricht: Optional[str] = None - wunsch_geschlecht: Optional[str] = None - wunsch_farbe: Optional[str] = None - prioritaet: Optional[int] = None - status: Optional[str] = None - notiz: Optional[str] = None - - -@router.get("/litters/{litter_id}/waitlist") -async def get_waitlist(litter_id: int, user=Depends(_require_breeder)): - with db() as conn: - _check_litter_owner(litter_id, user, conn) - rows = conn.execute( - "SELECT * FROM litter_waitlist WHERE litter_id=? ORDER BY prioritaet, created_at", - (litter_id,) - ).fetchall() - return [dict(r) for r in rows] - - -@router.post("/litters/{litter_id}/waitlist", status_code=201) -async def add_waitlist_entry(litter_id: int, body: WaitlistEntry, user=Depends(_require_breeder)): - with db() as conn: - _check_litter_owner(litter_id, user, conn) - cur = conn.execute( - """INSERT INTO litter_waitlist - (litter_id, name, email, telefon, nachricht, wunsch_geschlecht, wunsch_farbe, - prioritaet, status, notiz) - VALUES (?,?,?,?,?,?,?,?,?,?)""", - (litter_id, body.name, body.email, body.telefon, body.nachricht, - body.wunsch_geschlecht, body.wunsch_farbe, body.prioritaet, body.status, body.notiz) - ) - row = conn.execute("SELECT * FROM litter_waitlist WHERE id=?", (cur.lastrowid,)).fetchone() - return dict(row) - - -@router.put("/litters/waitlist/{entry_id}") -async def update_waitlist_entry(entry_id: int, body: WaitlistUpdate, user=Depends(_require_breeder)): - with db() as conn: - entry = conn.execute( - """SELECT w.*, bp.user_id AS owner_user_id FROM litter_waitlist w - JOIN litters l ON l.id = w.litter_id - JOIN breeder_profiles bp ON bp.id = l.breeder_id - WHERE w.id=?""", - (entry_id,) - ).fetchone() - if not entry: - raise HTTPException(404, "Eintrag nicht gefunden.") - if user["rolle"] != "admin" and entry["owner_user_id"] != user["id"]: - raise HTTPException(403, "Kein Zugriff.") - - fields = {k: v for k, v in body.model_dump().items() if v is not None} - if not fields: - return dict(entry) - sets = ", ".join(f"{k}=?" for k in fields) - conn.execute(f"UPDATE litter_waitlist SET {sets} WHERE id=?", (*fields.values(), entry_id)) - row = conn.execute("SELECT * FROM litter_waitlist WHERE id=?", (entry_id,)).fetchone() - return dict(row) - - -@router.delete("/litters/waitlist/{entry_id}", status_code=204) -async def delete_waitlist_entry(entry_id: int, user=Depends(_require_breeder)): - with db() as conn: - entry = conn.execute( - """SELECT w.id, bp.user_id AS owner_user_id FROM litter_waitlist w - JOIN litters l ON l.id = w.litter_id - JOIN breeder_profiles bp ON bp.id = l.breeder_id - WHERE w.id=?""", - (entry_id,) - ).fetchone() - if not entry: - raise HTTPException(404, "Eintrag nicht gefunden.") - if user["rolle"] != "admin" and entry["owner_user_id"] != user["id"]: - raise HTTPException(403, "Kein Zugriff.") - conn.execute("DELETE FROM litter_waitlist WHERE id=?", (entry_id,)) diff --git a/backend/static/css/components.css b/backend/static/css/components.css index 976aef5..0a6c754 100644 --- a/backend/static/css/components.css +++ b/backend/static/css/components.css @@ -6977,10 +6977,18 @@ svg.empty-state-icon { .wb-cards { display: grid; - grid-template-columns: repeat(auto-fill, minmax(340px, 1fr)); + grid-template-columns: 1fr; gap: var(--space-4); } +@media (min-width: 768px) { + .wb-cards { grid-template-columns: repeat(2, 1fr); } +} + +@media (min-width: 1200px) { + .wb-cards { grid-template-columns: repeat(3, 1fr); } +} + .wb-card { background: var(--c-surface); border: 1px solid var(--c-border-light); diff --git a/backend/static/index.html b/backend/static/index.html index 0e13659..cc215cf 100644 --- a/backend/static/index.html +++ b/backend/static/index.html @@ -101,9 +101,9 @@ - - - + + + @@ -248,25 +248,13 @@ Erste Hilfe -