Files
homekeeper/api/app/routers/colonies.py
T
2026-07-06 19:25:38 +02:00

295 lines
9.9 KiB
Python

from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy import text
from sqlalchemy.orm import Session
from typing import Any
from app.database import get_session
from app.auth import get_current_user
router = APIRouter()
def _row(r) -> dict:
return dict(r._mapping)
@router.get("")
def list_colonies(
apiary_id: int | None = Query(default=None),
include_closed: bool = Query(default=False),
session: Session = Depends(get_session),
_: str = Depends(get_current_user),
) -> list[dict]:
where = "1=1" if include_closed else "c.closed_at IS NULL"
params: dict[str, Any] = {}
if apiary_id is not None:
where += " AND c.apiary_id = :apiary_id"
params["apiary_id"] = apiary_id
rows = session.execute(text(f"""
SELECT
c.*,
a.name AS apiary_name,
p.name AS parent_name,
q.year AS queen_year,
q.breed AS queen_breed,
(SELECT MAX(i.date::DATE)
FROM inspections i
WHERE i.colony_id = c.id) AS last_inspection_date
FROM colonies c
LEFT JOIN apiaries a ON a.id = c.apiary_id
LEFT JOIN colonies p ON p.id = c.parent_colony_id
LEFT JOIN queens q ON q.id = c.current_queen_id
WHERE {where}
ORDER BY a.name, c.name
"""), params).fetchall()
return [_row(r) for r in rows]
@router.get("/log-placeholder", include_in_schema=False)
def _log_placeholder():
pass # just to force ordering; actual log route is below
@router.get("/{id}/log")
def colony_log(
id: int,
session: Session = Depends(get_session),
_: str = Depends(get_current_user),
) -> list[dict]:
rows = session.execute(text("""
SELECT date, type, summary, ref_id, created_at FROM (
SELECT date, 'durchsicht' AS type,
TRIM(BOTH FROM CONCAT_WS(' ',
CASE WHEN brood_frames IS NOT NULL THEN brood_frames::TEXT || ' BW' END,
CASE WHEN honey_frames IS NOT NULL THEN honey_frames::TEXT || ' HW' END,
CASE WHEN colony_strength IS NOT NULL THEN 'Stärke ' || colony_strength::TEXT END,
CASE WHEN queen_seen = 1 THEN '♛ gesehen' END
)) AS summary,
id AS ref_id, created_at
FROM inspections WHERE colony_id = :colony_id
UNION ALL
SELECT date, 'kontrolle' AS type,
count::TEXT || ' Milben' ||
CASE WHEN date_from IS NOT NULL
THEN ' · ' || (date::DATE - date_from::DATE)::TEXT || ' Tage' ELSE '' END AS summary,
id AS ref_id, created_at
FROM varroa_counts WHERE colony_id = :colony_id
UNION ALL
SELECT date, 'behandlung' AS type,
product || CASE WHEN dosage IS NOT NULL AND dosage <> ''
THEN ' · ' || dosage ELSE '' END AS summary,
id AS ref_id, created_at
FROM varroa_treatments WHERE colony_id = :colony_id
UNION ALL
SELECT vc.date, 'nachkontrolle' AS type,
vt.product || CASE WHEN vc.result IS NOT NULL AND vc.result <> ''
THEN ': ' || vc.result ELSE '' END AS summary,
vc.id AS ref_id, vc.created_at
FROM varroa_controls vc
JOIN varroa_treatments vt ON vt.id = vc.treatment_id
WHERE vc.colony_id = :colony_id
UNION ALL
SELECT h.date, 'ernte' AS type,
h.weight_kg::TEXT || ' kg' ||
CASE WHEN h.type IS NOT NULL THEN ' · ' || h.type ELSE '' END AS summary,
h.id AS ref_id, h.created_at
FROM honey_harvests h WHERE h.colony_id = :colony_id
UNION ALL
SELECT ca.date, 'massnahme' AS type,
CASE ca.type
WHEN 'ablegerbildung' THEN 'Ablegerbildung'
WHEN 'zwischenbodenableger' THEN 'Zwischenbödenableger'
WHEN 'brutentnahme' THEN 'Brutentnahme'
WHEN 'standwechsel' THEN 'Standwechsel'
WHEN 'futterung' THEN 'Fütterung'
WHEN 'vereinigung' THEN 'Völkervereinigung'
ELSE 'Maßnahme'
END ||
CASE WHEN ca.notes IS NOT NULL AND ca.notes <> ''
THEN ': ' || ca.notes ELSE '' END AS summary,
ca.id AS ref_id, ca.created_at
FROM colony_actions ca WHERE ca.colony_id = :colony_id
) log
ORDER BY date::DATE DESC, created_at DESC
"""), {"colony_id": id}).fetchall()
return [_row(r) for r in rows]
@router.get("/{id}/lineage")
def colony_lineage(
id: int,
session: Session = Depends(get_session),
_: str = Depends(get_current_user),
) -> list[dict]:
rows = session.execute(text("""
WITH RECURSIVE lineage(id, name, status, parent_colony_id, depth) AS (
SELECT id, name, status, parent_colony_id, 0
FROM colonies WHERE id = :colony_id
UNION ALL
SELECT c.id, c.name, c.status, c.parent_colony_id, l.depth + 1
FROM colonies c
JOIN lineage l ON c.id = l.parent_colony_id
)
SELECT * FROM lineage ORDER BY depth DESC
"""), {"colony_id": id}).fetchall()
return [_row(r) for r in rows]
@router.get("/{id}/descendants")
def colony_descendants(
id: int,
session: Session = Depends(get_session),
_: str = Depends(get_current_user),
) -> list[dict]:
rows = session.execute(text("""
WITH RECURSIVE descendants(id, name, status, parent_colony_id, depth) AS (
SELECT id, name, status, parent_colony_id, 0
FROM colonies WHERE id = :colony_id
UNION ALL
SELECT c.id, c.name, c.status, c.parent_colony_id, d.depth + 1
FROM colonies c
JOIN descendants d ON c.parent_colony_id = d.id
)
SELECT * FROM descendants WHERE depth > 0 ORDER BY depth, name
"""), {"colony_id": id}).fetchall()
return [_row(r) for r in rows]
@router.get("/{id}/location-history")
def colony_location_history(
id: int,
session: Session = Depends(get_session),
_: str = Depends(get_current_user),
) -> list[dict]:
rows = session.execute(text("""
SELECT cl.*, a.name AS apiary_name
FROM colony_locations cl
JOIN apiaries a ON a.id = cl.apiary_id
WHERE cl.colony_id = :colony_id
ORDER BY cl.moved_at DESC
"""), {"colony_id": id}).fetchall()
return [_row(r) for r in rows]
@router.get("/{id}")
def get_colony(
id: int,
session: Session = Depends(get_session),
_: str = Depends(get_current_user),
) -> dict:
row = session.execute(text("""
SELECT c.*, a.name AS apiary_name, p.name AS parent_name
FROM colonies c
LEFT JOIN apiaries a ON a.id = c.apiary_id
LEFT JOIN colonies p ON p.id = c.parent_colony_id
WHERE c.id = :id
"""), {"id": id}).fetchone()
if row is None:
raise HTTPException(status_code=404, detail="Not found")
return _row(row)
@router.post("", status_code=201)
def create_colony(
body: dict[str, Any],
session: Session = Depends(get_session),
_: str = Depends(get_current_user),
) -> dict:
row = session.execute(text("""
INSERT INTO colonies (name, apiary_id, status, parent_colony_id, notes)
VALUES (:name, :apiary_id, :status, :parent_colony_id, :notes)
RETURNING *
"""), {
"name": body.get("name"),
"apiary_id": body.get("apiary_id"),
"status": body.get("status", "wirtschaftsvolk"),
"parent_colony_id": body.get("parent_colony_id"),
"notes": body.get("notes"),
}).fetchone()
colony_id = row._mapping["id"]
session.execute(text("""
INSERT INTO colony_locations (colony_id, apiary_id) VALUES (:colony_id, :apiary_id)
"""), {"colony_id": colony_id, "apiary_id": body.get("apiary_id")})
session.commit()
return _row(row)
@router.put("/{id}")
def update_colony(
id: int,
body: dict[str, Any],
session: Session = Depends(get_session),
_: str = Depends(get_current_user),
) -> dict:
# Check if apiary changed (to insert colony_location)
old = session.execute(
text("SELECT apiary_id FROM colonies WHERE id = :id"), {"id": id}
).fetchone()
result = session.execute(text("""
UPDATE colonies
SET name = :name, apiary_id = :apiary_id, status = :status, notes = :notes
WHERE id = :id
RETURNING *
"""), {
"id": id,
"name": body.get("name"),
"apiary_id": body.get("apiary_id"),
"status": body.get("status"),
"notes": body.get("notes"),
}).fetchone()
if result is None:
raise HTTPException(status_code=404, detail="Not found")
new_apiary_id = body.get("apiary_id")
if old is not None and old._mapping["apiary_id"] != new_apiary_id:
session.execute(text("""
INSERT INTO colony_locations (colony_id, apiary_id) VALUES (:colony_id, :apiary_id)
"""), {"colony_id": id, "apiary_id": new_apiary_id})
session.commit()
return _row(result)
@router.post("/{id}/close")
def close_colony(
id: int,
session: Session = Depends(get_session),
_: str = Depends(get_current_user),
) -> dict:
result = session.execute(text("""
UPDATE colonies SET status = 'eingegangen', closed_at = NOW()
WHERE id = :id RETURNING *
"""), {"id": id}).fetchone()
session.commit()
if result is None:
raise HTTPException(status_code=404, detail="Not found")
return _row(result)
@router.post("/{id}/set-queen")
def set_queen(
id: int,
body: dict[str, Any],
session: Session = Depends(get_session),
_: str = Depends(get_current_user),
) -> dict:
result = session.execute(text("""
UPDATE colonies SET current_queen_id = :queen_id WHERE id = :id RETURNING *
"""), {"id": id, "queen_id": body.get("queen_id")}).fetchone()
session.commit()
if result is None:
raise HTTPException(status_code=404, detail="Not found")
return _row(result)