Files
Vorrania/backend/app/routers/views.py
Scarriffle b683f2d93b Web-UI: professionelles Redesign (Icons statt Emojis) + neue Features
Design:
- Neues Icon-Set (Inline-SVG, Feather-Stil), keine Emojis mehr
- Kohaerentes Design-System (Sidebar-Layout, Palette, Spacing, Light/Dark)
- Alle Seiten ueberarbeitet (Login, Dashboard, Produkte, Formular, Ein-/Auslagern,
  Lagerorte, Benutzer, Einkaufsliste)

Neue Features:
- Gruppen-Verwaltung (anlegen/bearbeiten/loeschen, Produktzahl + Bestand,
  Gruppen-Mindestbestand) inkl. Einkaufsliste
- Einfache Gruppen-Auto-Zuordnung aus OFF-Kategorie im Produktformular
- Verlauf-Seite (Bewegungen: wer/was/wann) via neuem /movements-Endpoint
- Einstellungen-Seite (Ablauf-Warnfrist)
- Lagerorte mit optional uebergeordnetem Ort (verschachtelbar)

Backend:
- groups: PATCH + Anreicherung (product_count, stock)
- views: /shopping-list/groups, /movements
- schemas: GroupUpdate, GroupShoppingItem, MovementOut

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 10:18:30 +02:00

145 lines
4.9 KiB
Python

from datetime import date, timedelta
from fastapi import APIRouter, Depends
from sqlalchemy.orm import Session
from ..database import get_db
from ..deps import get_current_user
from ..models import Group, Lot, Movement, Product, User
from ..schemas import ExpiringItem, GroupShoppingItem, MovementOut, ShoppingItem
from ..services.stock import current_stock
from .settings import get_expiry_warning_days
router = APIRouter(tags=["views"])
@router.get("/shopping-list", response_model=list[ShoppingItem])
def shopping_list(
db: Session = Depends(get_db), _: User = Depends(get_current_user)
) -> list[ShoppingItem]:
"""Produkte, deren Bestand unter dem Mindestbestand liegt."""
items: list[ShoppingItem] = []
products = (
db.query(Product)
.filter(Product.min_stock.isnot(None), Product.min_stock > 0)
.all()
)
for product in products:
stock = current_stock(db, product.id)
if stock < product.min_stock:
items.append(
ShoppingItem(
product_id=product.id,
name=product.name,
base_unit=product.base_unit,
stock=stock,
min_stock=product.min_stock,
deficit=product.min_stock - stock,
)
)
items.sort(key=lambda i: i.deficit, reverse=True)
return items
@router.get("/shopping-list/groups", response_model=list[GroupShoppingItem])
def group_shopping_list(
db: Session = Depends(get_db), _: User = Depends(get_current_user)
) -> list[GroupShoppingItem]:
"""Gruppen, deren Gesamtbestand unter dem Gruppen-Mindestbestand liegt.
Gruppen-Bestand = Summe der Produktbestände in der Gruppe (in Basiseinheiten).
Sinnvoll, wenn die Produkte einer Gruppe dieselbe Basiseinheit teilen.
"""
items: list[GroupShoppingItem] = []
groups = (
db.query(Group).filter(Group.min_stock.isnot(None), Group.min_stock > 0).all()
)
for group in groups:
products = db.query(Product).filter(Product.group_id == group.id).all()
stock = float(sum(current_stock(db, p.id) for p in products))
if stock < group.min_stock:
items.append(
GroupShoppingItem(
group_id=group.id,
name=group.name,
stock=stock,
min_stock=group.min_stock,
deficit=group.min_stock - stock,
product_count=len(products),
)
)
items.sort(key=lambda i: i.deficit, reverse=True)
return items
@router.get("/expiring", response_model=list[ExpiringItem])
def expiring(
days: int | None = None,
db: Session = Depends(get_db),
_: User = Depends(get_current_user),
) -> list[ExpiringItem]:
"""Chargen, die innerhalb der Warnfrist ablaufen (oder schon abgelaufen sind)."""
if days is None:
days = get_expiry_warning_days(db)
today = date.today()
threshold = today + timedelta(days=days)
lots = (
db.query(Lot)
.filter(Lot.best_before.isnot(None), Lot.best_before <= threshold, Lot.quantity > 0)
.order_by(Lot.best_before)
.all()
)
result: list[ExpiringItem] = []
for lot in lots:
product = lot.product
result.append(
ExpiringItem(
lot_id=lot.id,
product_id=product.id,
product_name=product.name,
quantity=lot.quantity,
base_unit=product.base_unit,
best_before=lot.best_before,
days_left=(lot.best_before - today).days,
)
)
return result
@router.get("/movements", response_model=list[MovementOut])
def movements(
limit: int = 100,
product_id: int | None = None,
db: Session = Depends(get_db),
_: User = Depends(get_current_user),
) -> list[MovementOut]:
"""Bewegungsverlauf (wer hat wann was ein-/ausgelagert), neueste zuerst."""
limit = max(1, min(limit, 500))
query = (
db.query(Movement, Product, User)
.join(Product, Movement.product_id == Product.id)
.outerjoin(User, Movement.user_id == User.id)
)
if product_id is not None:
query = query.filter(Movement.product_id == product_id)
rows = query.order_by(Movement.created_at.desc(), Movement.id.desc()).limit(limit).all()
result: list[MovementOut] = []
for movement, product, user in rows:
result.append(
MovementOut(
id=movement.id,
product_id=product.id,
product_name=product.name,
type=movement.type.value,
quantity=movement.quantity,
base_unit=product.base_unit,
unit_used=movement.unit_used,
username=user.username if user else None,
note=movement.note,
created_at=movement.created_at,
)
)
return result