Add teams + team ownership (schema + API)
This commit is contained in:
@@ -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")
|
||||
@@ -7,8 +7,15 @@ from sqlmodel import Session, select
|
||||
from app.api.utils import get_actor_employee_id, log_activity
|
||||
from app.db.session import get_session
|
||||
from app.integrations.openclaw import OpenClawClient
|
||||
from app.models.org import Department, Employee
|
||||
from app.schemas.org import DepartmentCreate, DepartmentUpdate, EmployeeCreate, EmployeeUpdate
|
||||
from app.models.org import Department, Team, Employee
|
||||
from app.schemas.org import (
|
||||
DepartmentCreate,
|
||||
DepartmentUpdate,
|
||||
TeamCreate,
|
||||
TeamUpdate,
|
||||
EmployeeCreate,
|
||||
EmployeeUpdate,
|
||||
)
|
||||
|
||||
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()
|
||||
|
||||
|
||||
@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)
|
||||
def create_department(
|
||||
payload: DepartmentCreate,
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
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.work import Task, TaskComment
|
||||
|
||||
__all__ = [
|
||||
"Department",
|
||||
"Employee",
|
||||
"Team",
|
||||
"Project",
|
||||
"ProjectMember",
|
||||
"Task",
|
||||
|
||||
@@ -13,6 +13,16 @@ class Department(SQLModel, table=True):
|
||||
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):
|
||||
__tablename__ = "employees"
|
||||
|
||||
@@ -21,6 +31,7 @@ class Employee(SQLModel, table=True):
|
||||
employee_type: str # human | agent
|
||||
|
||||
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")
|
||||
|
||||
title: str | None = None
|
||||
|
||||
@@ -10,6 +10,9 @@ class Project(SQLModel, table=True):
|
||||
name: str = Field(index=True, unique=True)
|
||||
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):
|
||||
__tablename__ = "project_members"
|
||||
|
||||
@@ -13,10 +13,23 @@ class DepartmentUpdate(SQLModel):
|
||||
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):
|
||||
name: str
|
||||
employee_type: str
|
||||
department_id: int | None = None
|
||||
team_id: int | None = None
|
||||
manager_id: int | None = None
|
||||
title: str | None = None
|
||||
status: str = "active"
|
||||
@@ -30,6 +43,7 @@ class EmployeeUpdate(SQLModel):
|
||||
name: str | None = None
|
||||
employee_type: str | None = None
|
||||
department_id: int | None = None
|
||||
team_id: int | None = None
|
||||
manager_id: int | None = None
|
||||
title: str | None = None
|
||||
status: str | None = None
|
||||
|
||||
@@ -6,8 +6,10 @@ from sqlmodel import SQLModel
|
||||
class ProjectCreate(SQLModel):
|
||||
name: str
|
||||
status: str = "active"
|
||||
team_id: int | None = None
|
||||
|
||||
|
||||
class ProjectUpdate(SQLModel):
|
||||
name: str | None = None
|
||||
status: str | None = None
|
||||
team_id: int | None = None
|
||||
|
||||
Reference in New Issue
Block a user