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)