from typing import Annotated from uuid import UUID from fastapi import APIRouter, Depends, HTTPException, status from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select, func from app.core.database import get_db from app.models.workspace import Workspace from app.models.tree import Tree from app.models.user import User from app.schemas.workspace import WorkspaceCreate, WorkspaceUpdate, WorkspaceResponse from app.api.deps import get_current_active_user router = APIRouter(prefix="/workspaces", tags=["workspaces"]) @router.get("", response_model=list[WorkspaceResponse]) async def list_workspaces( db: Annotated[AsyncSession, Depends(get_db)], current_user: Annotated[User, Depends(get_current_active_user)], ): """List all workspaces for the user's account.""" if not current_user.account_id: return [] # Get workspaces with tree counts query = ( select( Workspace, func.count(Tree.id).label("tree_count") ) .outerjoin(Tree, (Tree.workspace_id == Workspace.id) & (Tree.deleted_at.is_(None))) .where(Workspace.account_id == current_user.account_id) .group_by(Workspace.id) .order_by(Workspace.sort_order, Workspace.name) ) result = await db.execute(query) rows = result.all() return [ WorkspaceResponse( **{c.key: getattr(ws, c.key) for c in Workspace.__table__.columns}, tree_count=tree_count ) for ws, tree_count in rows ] @router.post("", response_model=WorkspaceResponse, status_code=status.HTTP_201_CREATED) async def create_workspace( data: WorkspaceCreate, db: Annotated[AsyncSession, Depends(get_db)], current_user: Annotated[User, Depends(get_current_active_user)], ): """Create a new workspace.""" if not current_user.account_id: raise HTTPException(status_code=400, detail="No account found") # Check slug uniqueness within account existing = await db.execute( select(Workspace).where( Workspace.account_id == current_user.account_id, Workspace.slug == data.slug ) ) if existing.scalar_one_or_none(): raise HTTPException(status_code=409, detail="Workspace slug already exists") workspace = Workspace( name=data.name, slug=data.slug, description=data.description, icon=data.icon, accent_color=data.accent_color, account_id=current_user.account_id, sort_order=data.sort_order, ) db.add(workspace) await db.flush() await db.commit() return WorkspaceResponse( **{c.key: getattr(workspace, c.key) for c in Workspace.__table__.columns}, tree_count=0 ) @router.patch("/{workspace_id}", response_model=WorkspaceResponse) async def update_workspace( workspace_id: UUID, data: WorkspaceUpdate, db: Annotated[AsyncSession, Depends(get_db)], current_user: Annotated[User, Depends(get_current_active_user)], ): """Update a workspace.""" workspace = await db.get(Workspace, workspace_id) if not workspace or workspace.account_id != current_user.account_id: raise HTTPException(status_code=404, detail="Workspace not found") update_data = data.model_dump(exclude_unset=True) if "slug" in update_data: existing = await db.execute( select(Workspace).where( Workspace.account_id == current_user.account_id, Workspace.slug == update_data["slug"], Workspace.id != workspace_id ) ) if existing.scalar_one_or_none(): raise HTTPException(status_code=409, detail="Workspace slug already exists") for key, value in update_data.items(): setattr(workspace, key, value) await db.commit() # Get tree count count_result = await db.execute( select(func.count(Tree.id)).where( Tree.workspace_id == workspace_id, Tree.deleted_at.is_(None) ) ) tree_count = count_result.scalar() or 0 return WorkspaceResponse( **{c.key: getattr(workspace, c.key) for c in Workspace.__table__.columns}, tree_count=tree_count ) @router.delete("/{workspace_id}", status_code=status.HTTP_204_NO_CONTENT) async def delete_workspace( workspace_id: UUID, db: Annotated[AsyncSession, Depends(get_db)], current_user: Annotated[User, Depends(get_current_active_user)], ): """Delete a workspace. Trees are unassigned, not deleted.""" workspace = await db.get(Workspace, workspace_id) if not workspace or workspace.account_id != current_user.account_id: raise HTTPException(status_code=404, detail="Workspace not found") if workspace.is_default: raise HTTPException(status_code=400, detail="Cannot delete the default workspace") # Unassign trees (set workspace_id to NULL) await db.execute( Tree.__table__.update() .where(Tree.workspace_id == workspace_id) .values(workspace_id=None) ) await db.delete(workspace) await db.commit()