Add teams + team ownership (schema + API)

This commit is contained in:
Jarvis
2026-02-02 12:51:25 +00:00
parent dc8750353d
commit e283543ef1
7 changed files with 178 additions and 3 deletions

View File

@@ -0,0 +1,73 @@
"""Add teams and team ownership
Revision ID: 3f2c1b9c8e12
Revises: bacd5e6a253d
Create Date: 2026-02-02
"""
from __future__ import annotations
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = "3f2c1b9c8e12"
down_revision = "bacd5e6a253d"
branch_labels = None
depends_on = None
def upgrade() -> None:
# 1) Teams
op.create_table(
"teams",
sa.Column("id", sa.Integer(), primary_key=True, nullable=False),
sa.Column("name", sa.String(), nullable=False),
sa.Column("department_id", sa.Integer(), nullable=False),
sa.Column("lead_employee_id", sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(["department_id"], ["departments.id"], ondelete="CASCADE"),
sa.ForeignKeyConstraint(["lead_employee_id"], ["employees.id"], ondelete="SET NULL"),
sa.UniqueConstraint("department_id", "name", name="uq_teams_department_id_name"),
)
op.create_index("ix_teams_name", "teams", ["name"], unique=False)
op.create_index("ix_teams_department_id", "teams", ["department_id"], unique=False)
# 2) Employees belong to one (optional) team
op.add_column("employees", sa.Column("team_id", sa.Integer(), nullable=True))
op.create_index("ix_employees_team_id", "employees", ["team_id"], unique=False)
op.create_foreign_key(
"fk_employees_team_id_teams",
"employees",
"teams",
["team_id"],
["id"],
ondelete="SET NULL",
)
# 3) Projects are owned by teams (not departments)
op.add_column("projects", sa.Column("team_id", sa.Integer(), nullable=True))
op.create_index("ix_projects_team_id", "projects", ["team_id"], unique=False)
op.create_foreign_key(
"fk_projects_team_id_teams",
"projects",
"teams",
["team_id"],
["id"],
ondelete="SET NULL",
)
def downgrade() -> None:
op.drop_constraint("fk_projects_team_id_teams", "projects", type_="foreignkey")
op.drop_index("ix_projects_team_id", table_name="projects")
op.drop_column("projects", "team_id")
op.drop_constraint("fk_employees_team_id_teams", "employees", type_="foreignkey")
op.drop_index("ix_employees_team_id", table_name="employees")
op.drop_column("employees", "team_id")
op.drop_index("ix_teams_department_id", table_name="teams")
op.drop_index("ix_teams_name", table_name="teams")
op.drop_table("teams")

View File

@@ -7,8 +7,15 @@ from sqlmodel import Session, select
from app.api.utils import get_actor_employee_id, log_activity from app.api.utils import get_actor_employee_id, log_activity
from app.db.session import get_session from app.db.session import get_session
from app.integrations.openclaw import OpenClawClient from app.integrations.openclaw import OpenClawClient
from app.models.org import Department, Employee from app.models.org import Department, Team, Employee
from app.schemas.org import DepartmentCreate, DepartmentUpdate, EmployeeCreate, EmployeeUpdate from app.schemas.org import (
DepartmentCreate,
DepartmentUpdate,
TeamCreate,
TeamUpdate,
EmployeeCreate,
EmployeeUpdate,
)
router = APIRouter(tags=["org"]) router = APIRouter(tags=["org"])
@@ -127,6 +134,70 @@ def list_departments(session: Session = Depends(get_session)):
return session.exec(select(Department).order_by(Department.name.asc())).all() return session.exec(select(Department).order_by(Department.name.asc())).all()
@router.get("/teams", response_model=list[Team])
def list_teams(department_id: int | None = None, session: Session = Depends(get_session)):
q = select(Team)
if department_id is not None:
q = q.where(Team.department_id == department_id)
return session.exec(q.order_by(Team.name.asc())).all()
@router.post("/teams", response_model=Team)
def create_team(
payload: TeamCreate,
session: Session = Depends(get_session),
actor_employee_id: int = Depends(get_actor_employee_id),
):
team = Team(**payload.model_dump())
session.add(team)
try:
session.flush()
log_activity(
session,
actor_employee_id=actor_employee_id,
entity_type="team",
entity_id=team.id,
verb="created",
payload={"name": team.name, "department_id": team.department_id, "lead_employee_id": team.lead_employee_id},
)
session.commit()
except IntegrityError:
session.rollback()
raise HTTPException(status_code=409, detail="Team already exists or violates constraints")
session.refresh(team)
return team
@router.patch("/teams/{team_id}", response_model=Team)
def update_team(
team_id: int,
payload: TeamUpdate,
session: Session = Depends(get_session),
actor_employee_id: int = Depends(get_actor_employee_id),
):
team = session.get(Team, team_id)
if not team:
raise HTTPException(status_code=404, detail="Team not found")
data = payload.model_dump(exclude_unset=True)
for k, v in data.items():
setattr(team, k, v)
session.add(team)
try:
session.flush()
log_activity(session, actor_employee_id=actor_employee_id, entity_type="team", entity_id=team.id, verb="updated", payload=data)
session.commit()
except IntegrityError:
session.rollback()
raise HTTPException(status_code=409, detail="Team update violates constraints")
session.refresh(team)
return team
@router.post("/departments", response_model=Department) @router.post("/departments", response_model=Department)
def create_department( def create_department(
payload: DepartmentCreate, payload: DepartmentCreate,

View File

@@ -1,11 +1,12 @@
from app.models.activity import Activity from app.models.activity import Activity
from app.models.org import Department, Employee from app.models.org import Department, Team, Employee
from app.models.projects import Project, ProjectMember from app.models.projects import Project, ProjectMember
from app.models.work import Task, TaskComment from app.models.work import Task, TaskComment
__all__ = [ __all__ = [
"Department", "Department",
"Employee", "Employee",
"Team",
"Project", "Project",
"ProjectMember", "ProjectMember",
"Task", "Task",

View File

@@ -13,6 +13,16 @@ class Department(SQLModel, table=True):
head_employee_id: int | None = Field(default=None, foreign_key="employees.id") head_employee_id: int | None = Field(default=None, foreign_key="employees.id")
class Team(SQLModel, table=True):
__tablename__ = "teams"
id: int | None = Field(default=None, primary_key=True)
name: str = Field(index=True)
department_id: int = Field(foreign_key="departments.id")
lead_employee_id: int | None = Field(default=None, foreign_key="employees.id")
class Employee(SQLModel, table=True): class Employee(SQLModel, table=True):
__tablename__ = "employees" __tablename__ = "employees"
@@ -21,6 +31,7 @@ class Employee(SQLModel, table=True):
employee_type: str # human | agent employee_type: str # human | agent
department_id: int | None = Field(default=None, foreign_key="departments.id") department_id: int | None = Field(default=None, foreign_key="departments.id")
team_id: int | None = Field(default=None, foreign_key="teams.id")
manager_id: int | None = Field(default=None, foreign_key="employees.id") manager_id: int | None = Field(default=None, foreign_key="employees.id")
title: str | None = None title: str | None = None

View File

@@ -10,6 +10,9 @@ class Project(SQLModel, table=True):
name: str = Field(index=True, unique=True) name: str = Field(index=True, unique=True)
status: str = Field(default="active") status: str = Field(default="active")
# Project ownership: projects are assigned to teams (not departments)
team_id: int | None = Field(default=None, foreign_key="teams.id")
class ProjectMember(SQLModel, table=True): class ProjectMember(SQLModel, table=True):
__tablename__ = "project_members" __tablename__ = "project_members"

View File

@@ -13,10 +13,23 @@ class DepartmentUpdate(SQLModel):
head_employee_id: int | None = None head_employee_id: int | None = None
class TeamCreate(SQLModel):
name: str
department_id: int
lead_employee_id: int | None = None
class TeamUpdate(SQLModel):
name: str | None = None
department_id: int | None = None
lead_employee_id: int | None = None
class EmployeeCreate(SQLModel): class EmployeeCreate(SQLModel):
name: str name: str
employee_type: str employee_type: str
department_id: int | None = None department_id: int | None = None
team_id: int | None = None
manager_id: int | None = None manager_id: int | None = None
title: str | None = None title: str | None = None
status: str = "active" status: str = "active"
@@ -30,6 +43,7 @@ class EmployeeUpdate(SQLModel):
name: str | None = None name: str | None = None
employee_type: str | None = None employee_type: str | None = None
department_id: int | None = None department_id: int | None = None
team_id: int | None = None
manager_id: int | None = None manager_id: int | None = None
title: str | None = None title: str | None = None
status: str | None = None status: str | None = None

View File

@@ -6,8 +6,10 @@ from sqlmodel import SQLModel
class ProjectCreate(SQLModel): class ProjectCreate(SQLModel):
name: str name: str
status: str = "active" status: str = "active"
team_id: int | None = None
class ProjectUpdate(SQLModel): class ProjectUpdate(SQLModel):
name: str | None = None name: str | None = None
status: str | None = None status: str | None = None
team_id: int | None = None