"""Verwendungsstatus von Einzelstücken: umstellen und Verlauf. Woran ist das Stück gerade? Getragen, in der Wäsche, in Reparatur – oder einfach verfügbar. Gedacht für eine Garderobe, aber bewusst allgemein: ein Auto in der Waschanlage ist genauso „in Reinigung". Umstellen ist eine **Buchung**, keine Stammdatenänderung – deshalb darf jeder angemeldete Benutzer es tun (wie Verleihen, Ein- und Auslagern), und es wird mit dem Konto protokolliert. Ausdrücklich **nicht** über ``PATCH /items/{id}``: Diese Route ist Admins vorbehalten, und die App schickt dort immer alle Felder mitsamt ``null`` (siehe ``ItemUpdateRequest.encode`` in ios/Sources/Models.swift) – jedes beliebige Speichern würde den Status stillschweigend zurücksetzen. Verleih und Zubehör haben aus demselben Grund eigene Endpunkte. Bewusst **keine** Bewegung (``Movement``), aus demselben Grund wie beim Verleih (siehe ``routers/loans.py``). Und bewusst **keine** Auswirkung auf den Bestand: Ein getragenes Hemd ist weiterhin im Haushalt – anders als ein verliehenes. """ from datetime import UTC, datetime from fastapi import APIRouter, Depends, HTTPException, status from sqlalchemy.orm import Session, joinedload from ..database import get_db from ..deps import get_current_user from ..models import Item, ItemUsage, User from ..schemas import ItemOut, VerwendungOut, VerwendungSetzen from ..services.items import item_to_out, verwendung_to_out router = APIRouter(tags=["verwendung"]) def _item_or_404(db: Session, item_id: int) -> Item: item = db.get(Item, item_id) if item is None: raise HTTPException(status.HTTP_404_NOT_FOUND, "Einzelstück nicht gefunden") return item def _laufend(db: Session, item_id: int) -> ItemUsage | None: """Der laufende Zeitraum dieses Stücks, falls einer offen ist.""" return ( db.query(ItemUsage) .filter(ItemUsage.item_id == item_id, ItemUsage.ended_at.is_(None)) .first() ) @router.put("/items/{item_id}/verwendung", response_model=ItemOut) def setze_verwendung( item_id: int, payload: VerwendungSetzen, db: Session = Depends(get_db), user: User = Depends(get_current_user), ) -> ItemOut: """Verwendungszustand setzen. Leerer Status = verfügbar. Eine Route für alle Zustände, auch für den Ruhezustand: „verfügbar" ist nichts anderes als „kein laufender Zeitraum", also beendet der leere Status nur den aktuellen. Damit ist der Aufruf idempotent. Antwortet mit dem ganzen Stück, damit die Oberflächen nach der Buchung nicht noch einmal laden müssen. """ item = _item_or_404(db, item_id) neu = payload.status.value if payload.status is not None else None laufend = _laufend(db, item.id) if laufend is not None and laufend.status == neu: # Zweimal dasselbe angetippt – sonst entstünde ein Zeitraum der Länge 0. return item_to_out(item) if laufend is not None: laufend.ended_at = datetime.now(UTC) # Vor dem Einfügen schreiben: Sonst darf SQLAlchemy das INSERT vor das # UPDATE stellen, und dann liegen kurzzeitig zwei offene Zeiträume an – # der Teilindex uq_item_usage_offen schlägt zu. db.flush() if neu is not None: db.add(ItemUsage(item_id=item.id, status=neu, user_id=user.id)) db.commit() db.refresh(item) return item_to_out(item) @router.get("/items/{item_id}/verwendungen", response_model=list[VerwendungOut]) def list_verwendungen( item_id: int, db: Session = Depends(get_db), _: User = Depends(get_current_user), ) -> list[VerwendungOut]: """Der ganze Verlauf dieses Stücks, neueste zuerst.""" _item_or_404(db, item_id) rows = ( db.query(ItemUsage) .options(joinedload(ItemUsage.user)) .filter(ItemUsage.item_id == item_id) .order_by(ItemUsage.started_at.desc(), ItemUsage.id.desc()) .all() ) return [verwendung_to_out(u) for u in rows]