- Trainings-Streak: streak.py, DB training_streaks, Scheduler 19:00, Widget in welcome.js, Ping in uebungen.js
- Ausgaben-Tracker: expenses.py, expenses.js, DB expenses-Tabelle
- KI-Tierarztfragen: ki.py /tierarzt, health.js Button+Modal, DB ki_tierarzt_log
- Rückruf-Alarm: recalls.py, recalls.js, DB feed_recalls, Scheduler 08:00 RASFF
- Adoption: adoption.py, adoption.js, DB adoption_cache
- Tierarzt-Favorit + Befunde: tieraerzte.py /my-favorite+/favorite, health_docs.py, health.js, api.js, DB favorite_vets+health_documents
- Digitaler Hundepass: passport.py, dog-profile.js, main.py /pass/{token}, DB vaccinations+medications+dog_passport_meta+passport_shares, requirements.txt fpdf2
- Playdate-Matching: playdate.py, playdate.js, DB playdate_listings+playdate_requests
- Rassen-Erkennung: ki.py /rasse-erkennung (Claude Vision), dog-profile.js+wiki.js, CSS .rasse-result-card, DB ki_rasse_log
228 lines
7.7 KiB
Python
228 lines
7.7 KiB
Python
"""BAN YARO — Ausgaben-Tracker Routes"""
|
|
|
|
import logging
|
|
from datetime import date
|
|
from fastapi import APIRouter, Depends, HTTPException, Query
|
|
from pydantic import BaseModel
|
|
from typing import Optional
|
|
from database import db
|
|
from auth import get_current_user
|
|
|
|
router = APIRouter()
|
|
logger = logging.getLogger(__name__)
|
|
|
|
KATEGORIEN = {"tierarzt", "futter", "zubehoer", "versicherung", "sitter", "sonstiges"}
|
|
|
|
|
|
# ------------------------------------------------------------------
|
|
# Schemas
|
|
# ------------------------------------------------------------------
|
|
class ExpenseCreate(BaseModel):
|
|
dog_id: Optional[int] = None
|
|
kategorie: str
|
|
betrag: float
|
|
datum: str
|
|
notiz: Optional[str] = None
|
|
|
|
|
|
class ExpenseUpdate(BaseModel):
|
|
dog_id: Optional[int] = None
|
|
kategorie: Optional[str] = None
|
|
betrag: Optional[float] = None
|
|
datum: Optional[str] = None
|
|
notiz: Optional[str] = None
|
|
|
|
|
|
def _serialize(row) -> dict:
|
|
return dict(row)
|
|
|
|
|
|
# ------------------------------------------------------------------
|
|
# GET /api/expenses/summary — Monats- und Jahressummen
|
|
# WICHTIG: Diese Route muss VOR /{id} stehen!
|
|
# ------------------------------------------------------------------
|
|
@router.get("/summary")
|
|
async def get_summary(
|
|
dog_id: Optional[int] = Query(default=None),
|
|
user=Depends(get_current_user),
|
|
):
|
|
today = date.today()
|
|
monat_prefix = today.strftime("%Y-%m")
|
|
jahr_prefix = today.strftime("%Y")
|
|
|
|
extra_cond = ""
|
|
extra_params: list = []
|
|
if dog_id is not None:
|
|
extra_cond = " AND dog_id=?"
|
|
extra_params = [dog_id]
|
|
|
|
with db() as conn:
|
|
# Monats-Summen pro Kategorie
|
|
rows_monat = conn.execute(
|
|
f"""SELECT kategorie, COALESCE(SUM(betrag),0) AS summe
|
|
FROM expenses
|
|
WHERE user_id=? AND datum LIKE ?{extra_cond}
|
|
GROUP BY kategorie""",
|
|
[user["id"], f"{monat_prefix}%"] + extra_params,
|
|
).fetchall()
|
|
|
|
# Jahres-Summen pro Kategorie
|
|
rows_jahr = conn.execute(
|
|
f"""SELECT kategorie, COALESCE(SUM(betrag),0) AS summe
|
|
FROM expenses
|
|
WHERE user_id=? AND datum LIKE ?{extra_cond}
|
|
GROUP BY kategorie""",
|
|
[user["id"], f"{jahr_prefix}%"] + extra_params,
|
|
).fetchall()
|
|
|
|
monat = {r["kategorie"]: round(r["summe"], 2) for r in rows_monat}
|
|
jahr = {r["kategorie"]: round(r["summe"], 2) for r in rows_jahr}
|
|
|
|
gesamt_monat = round(sum(monat.values()), 2)
|
|
gesamt_jahr = round(sum(jahr.values()), 2)
|
|
|
|
return {
|
|
"monat": monat,
|
|
"jahr": jahr,
|
|
"gesamt_monat": gesamt_monat,
|
|
"gesamt_jahr": gesamt_jahr,
|
|
}
|
|
|
|
|
|
# ------------------------------------------------------------------
|
|
# GET /api/expenses — Liste mit optionalen Filtern
|
|
# ------------------------------------------------------------------
|
|
@router.get("")
|
|
async def list_expenses(
|
|
dog_id: Optional[int] = Query(default=None),
|
|
von: Optional[str] = Query(default=None),
|
|
bis: Optional[str] = Query(default=None),
|
|
limit: int = Query(default=100, le=500),
|
|
offset: int = Query(default=0),
|
|
user=Depends(get_current_user),
|
|
):
|
|
conditions = ["e.user_id=?"]
|
|
params: list = [user["id"]]
|
|
|
|
if dog_id is not None:
|
|
conditions.append("e.dog_id=?")
|
|
params.append(dog_id)
|
|
if von:
|
|
conditions.append("e.datum >= ?")
|
|
params.append(von)
|
|
if bis:
|
|
conditions.append("e.datum <= ?")
|
|
params.append(bis)
|
|
|
|
where = " AND ".join(conditions)
|
|
params += [limit, offset]
|
|
|
|
with db() as conn:
|
|
rows = conn.execute(
|
|
f"""SELECT e.*, d.name AS dog_name
|
|
FROM expenses e
|
|
LEFT JOIN dogs d ON d.id = e.dog_id
|
|
WHERE {where}
|
|
ORDER BY e.datum DESC, e.id DESC
|
|
LIMIT ? OFFSET ?""",
|
|
params,
|
|
).fetchall()
|
|
|
|
return [_serialize(r) for r in rows]
|
|
|
|
|
|
# ------------------------------------------------------------------
|
|
# POST /api/expenses — neuer Eintrag
|
|
# ------------------------------------------------------------------
|
|
@router.post("", status_code=201)
|
|
async def create_expense(data: ExpenseCreate, user=Depends(get_current_user)):
|
|
if data.kategorie not in KATEGORIEN:
|
|
raise HTTPException(400, f"Ungültige Kategorie: {data.kategorie}")
|
|
if data.betrag <= 0:
|
|
raise HTTPException(400, "Betrag muss größer als 0 sein.")
|
|
|
|
with db() as conn:
|
|
# dog_id prüfen — muss dem User gehören
|
|
if data.dog_id is not None:
|
|
dog = conn.execute(
|
|
"SELECT id FROM dogs WHERE id=? AND user_id=?",
|
|
(data.dog_id, user["id"]),
|
|
).fetchone()
|
|
if not dog:
|
|
raise HTTPException(404, "Hund nicht gefunden.")
|
|
|
|
conn.execute(
|
|
"""INSERT INTO expenses (user_id, dog_id, kategorie, betrag, datum, notiz)
|
|
VALUES (?, ?, ?, ?, ?, ?)""",
|
|
(user["id"], data.dog_id, data.kategorie, data.betrag, data.datum, data.notiz),
|
|
)
|
|
row = conn.execute(
|
|
"SELECT * FROM expenses WHERE user_id=? ORDER BY id DESC LIMIT 1",
|
|
(user["id"],),
|
|
).fetchone()
|
|
|
|
return _serialize(row)
|
|
|
|
|
|
# ------------------------------------------------------------------
|
|
# PATCH /api/expenses/{id} — bearbeiten
|
|
# ------------------------------------------------------------------
|
|
@router.patch("/{expense_id}")
|
|
async def update_expense(
|
|
expense_id: int, data: ExpenseUpdate, user=Depends(get_current_user)
|
|
):
|
|
with db() as conn:
|
|
row = conn.execute(
|
|
"SELECT * FROM expenses WHERE id=? AND user_id=?",
|
|
(expense_id, user["id"]),
|
|
).fetchone()
|
|
if not row:
|
|
raise HTTPException(404, "Eintrag nicht gefunden.")
|
|
|
|
updates = {}
|
|
if data.kategorie is not None:
|
|
if data.kategorie not in KATEGORIEN:
|
|
raise HTTPException(400, f"Ungültige Kategorie: {data.kategorie}")
|
|
updates["kategorie"] = data.kategorie
|
|
if data.betrag is not None:
|
|
if data.betrag <= 0:
|
|
raise HTTPException(400, "Betrag muss größer als 0 sein.")
|
|
updates["betrag"] = data.betrag
|
|
if data.datum is not None:
|
|
updates["datum"] = data.datum
|
|
if data.notiz is not None:
|
|
updates["notiz"] = data.notiz
|
|
if data.dog_id is not None:
|
|
dog = conn.execute(
|
|
"SELECT id FROM dogs WHERE id=? AND user_id=?",
|
|
(data.dog_id, user["id"]),
|
|
).fetchone()
|
|
if not dog:
|
|
raise HTTPException(404, "Hund nicht gefunden.")
|
|
updates["dog_id"] = data.dog_id
|
|
|
|
if not updates:
|
|
return _serialize(row)
|
|
|
|
set_clause = ", ".join(f"{k}=?" for k in updates)
|
|
values = list(updates.values()) + [expense_id]
|
|
conn.execute(f"UPDATE expenses SET {set_clause} WHERE id=?", values)
|
|
row = conn.execute("SELECT * FROM expenses WHERE id=?", (expense_id,)).fetchone()
|
|
|
|
return _serialize(row)
|
|
|
|
|
|
# ------------------------------------------------------------------
|
|
# DELETE /api/expenses/{id} — löschen
|
|
# ------------------------------------------------------------------
|
|
@router.delete("/{expense_id}", status_code=204)
|
|
async def delete_expense(expense_id: int, user=Depends(get_current_user)):
|
|
with db() as conn:
|
|
row = conn.execute(
|
|
"SELECT id FROM expenses WHERE id=? AND user_id=?",
|
|
(expense_id, user["id"]),
|
|
).fetchone()
|
|
if not row:
|
|
raise HTTPException(404, "Eintrag nicht gefunden.")
|
|
conn.execute("DELETE FROM expenses WHERE id=?", (expense_id,))
|
|
return None
|