Backend (FastAPI): - REST API: auth, plants, beds, plantings - CRUD layer with CRUDBase - Pydantic v2 schemas for all entities - Alembic migration: complete schema + all enums - Seed data: 28 global plants + 15 compatibilities Frontend (Vue 3 + PrimeVue): - Axios client with JWT interceptor + auto-refresh - Pinia stores: auth, beds, plants - Views: Login, Beds, BedDetail, PlantLibrary - Components: AppLayout, BedForm, PlantingForm, PlantForm Docker: - docker-compose.yml (production) - docker-compose.dev.yml (development with hot-reload) - Nginx config with SPA fallback + API proxy - Multi-stage frontend Dockerfile - .env.example, .gitignore Version: 1.0.0-alpha
79 lines
2.9 KiB
Python
79 lines
2.9 KiB
Python
from typing import Annotated
|
|
from uuid import UUID
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, status
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.core.deps import get_session, get_tenant_context, require_min_role
|
|
from app.crud.bed import crud_bed
|
|
from app.crud.planting import crud_planting
|
|
from app.models.user import TenantRole
|
|
from app.schemas.planting import PlantingCreate, PlantingRead, PlantingUpdate
|
|
|
|
router = APIRouter(tags=["Bepflanzungen"])
|
|
|
|
TenantCtx = Annotated[tuple, Depends(get_tenant_context)]
|
|
WriteCtx = Annotated[tuple, Depends(require_min_role(TenantRole.READ_WRITE))]
|
|
|
|
|
|
async def _get_bed_or_404(db: AsyncSession, bed_id: UUID, tenant_id: UUID):
|
|
bed = await crud_bed.get(db, id=bed_id)
|
|
if not bed or bed.tenant_id != tenant_id:
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Beet nicht gefunden.")
|
|
return bed
|
|
|
|
|
|
@router.get("/beds/{bed_id}/plantings", response_model=list[PlantingRead])
|
|
async def list_plantings(
|
|
bed_id: UUID,
|
|
db: Annotated[AsyncSession, Depends(get_session)],
|
|
ctx: TenantCtx,
|
|
) -> list[PlantingRead]:
|
|
_, tenant_id = ctx
|
|
await _get_bed_or_404(db, bed_id, tenant_id)
|
|
plantings = await crud_planting.get_multi_for_bed(db, bed_id=bed_id)
|
|
return [PlantingRead.model_validate(p) for p in plantings]
|
|
|
|
|
|
@router.post("/beds/{bed_id}/plantings", response_model=PlantingRead, status_code=status.HTTP_201_CREATED)
|
|
async def create_planting(
|
|
bed_id: UUID,
|
|
body: PlantingCreate,
|
|
db: Annotated[AsyncSession, Depends(get_session)],
|
|
ctx: WriteCtx,
|
|
) -> PlantingRead:
|
|
_, tenant_id, _ = ctx
|
|
await _get_bed_or_404(db, bed_id, tenant_id)
|
|
planting = await crud_planting.create_for_bed(db, obj_in=body, bed_id=bed_id)
|
|
return PlantingRead.model_validate(planting)
|
|
|
|
|
|
@router.put("/plantings/{planting_id}", response_model=PlantingRead)
|
|
async def update_planting(
|
|
planting_id: UUID,
|
|
body: PlantingUpdate,
|
|
db: Annotated[AsyncSession, Depends(get_session)],
|
|
ctx: WriteCtx,
|
|
) -> PlantingRead:
|
|
_, tenant_id, _ = ctx
|
|
planting = await crud_planting.get(db, id=planting_id)
|
|
if not planting:
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Bepflanzung nicht gefunden.")
|
|
await _get_bed_or_404(db, planting.bed_id, tenant_id)
|
|
updated = await crud_planting.update(db, db_obj=planting, obj_in=body)
|
|
return PlantingRead.model_validate(updated)
|
|
|
|
|
|
@router.delete("/plantings/{planting_id}", status_code=status.HTTP_204_NO_CONTENT)
|
|
async def delete_planting(
|
|
planting_id: UUID,
|
|
db: Annotated[AsyncSession, Depends(get_session)],
|
|
ctx: WriteCtx,
|
|
) -> None:
|
|
_, tenant_id, _ = ctx
|
|
planting = await crud_planting.get(db, id=planting_id)
|
|
if not planting:
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Bepflanzung nicht gefunden.")
|
|
await _get_bed_or_404(db, planting.bed_id, tenant_id)
|
|
await crud_planting.remove(db, id=planting_id)
|