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.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,
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user