From ef2676fa1c7e78965941864f57743f9df852fa76 Mon Sep 17 00:00:00 2001 From: Abhimanyu Saharan Date: Mon, 2 Feb 2026 18:59:54 +0530 Subject: [PATCH] Add Teams (DB + API + UI) --- README.md | 2 +- ...2c1b9c8e12_add_teams_and_team_ownership.py | 73 +++ backend/app/api/org.py | 76 ++- .../core/__pycache__/config.cpython-312.pyc | Bin 693 -> 677 bytes .../db/__pycache__/session.cpython-312.pyc | Bin 944 -> 928 bytes backend/app/models/__init__.py | 3 +- backend/app/models/org.py | 11 + backend/app/models/projects.py | 3 + backend/app/schemas/org.py | 14 + backend/app/schemas/projects.py | 2 + frontend/src/api/generated/model/employee.ts | 1 + .../src/api/generated/model/employeeCreate.ts | 1 + .../src/api/generated/model/employeeUpdate.ts | 1 + frontend/src/api/generated/model/index.ts | 4 + .../model/listTeamsTeamsGetParams.ts | 10 + frontend/src/api/generated/model/project.ts | 1 + .../src/api/generated/model/projectCreate.ts | 1 + .../src/api/generated/model/projectUpdate.ts | 1 + frontend/src/api/generated/model/team.ts | 13 + .../src/api/generated/model/teamCreate.ts | 12 + .../src/api/generated/model/teamUpdate.ts | 12 + frontend/src/api/generated/org/org.ts | 438 ++++++++++++++++++ frontend/src/app/_components/Shell.tsx | 1 + frontend/src/app/people/page.tsx | 23 + frontend/src/app/projects/page.tsx | 17 +- frontend/src/app/teams/page.tsx | 150 ++++++ 26 files changed, 865 insertions(+), 5 deletions(-) create mode 100644 backend/alembic/versions/3f2c1b9c8e12_add_teams_and_team_ownership.py create mode 100644 frontend/src/api/generated/model/listTeamsTeamsGetParams.ts create mode 100644 frontend/src/api/generated/model/team.ts create mode 100644 frontend/src/api/generated/model/teamCreate.ts create mode 100644 frontend/src/api/generated/model/teamUpdate.ts create mode 100644 frontend/src/app/teams/page.tsx diff --git a/README.md b/README.md index 0260903f..a8c59fd4 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ No auth (yet). The goal is simple visibility: everyone can see what exists and w Uses local Postgres: - user: `postgres` -- password: `netbox` +- password: `REDACTED` - db: `openclaw_agency` ## Environment diff --git a/backend/alembic/versions/3f2c1b9c8e12_add_teams_and_team_ownership.py b/backend/alembic/versions/3f2c1b9c8e12_add_teams_and_team_ownership.py new file mode 100644 index 00000000..0898fad3 --- /dev/null +++ b/backend/alembic/versions/3f2c1b9c8e12_add_teams_and_team_ownership.py @@ -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") diff --git a/backend/app/api/org.py b/backend/app/api/org.py index 571a4894..8fa88413 100644 --- a/backend/app/api/org.py +++ b/backend/app/api/org.py @@ -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,71 @@ 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, diff --git a/backend/app/core/__pycache__/config.cpython-312.pyc b/backend/app/core/__pycache__/config.cpython-312.pyc index 0392db7f7f71da8271d775a250b7f7415f011803..29ad631d1dcf6e941c86e23f0552e463f5ff4b1c 100644 GIT binary patch delta 40 ucmdnWx|EgsG%qg~0}vQ2ZrI2@lab$2KO;XkRX?#fF(a`kF>mr=#!mp>FAU!R delta 56 zcmZ3=x|NmtG%qg~0}v>;)^FsV$tdrspOK%Ns-IY#n2}hNn5Q35nG9s)1{CFIr6!jY JPu|S<2>{Wl6A%CZ diff --git a/backend/app/db/__pycache__/session.cpython-312.pyc b/backend/app/db/__pycache__/session.cpython-312.pyc index 7d7d28362e8b937a41e70d7459061dea9b747208..0d1077b347bf6e8eddc53bc97a942565b7d514c7 100644 GIT binary patch delta 41 vcmdnMzJQ(kG%qg~0}vQ2ZrI43!N_l+pOK%Ns-IY#n2}hNn74TnqZ|_eZVi4w#B diff --git a/backend/app/models/__init__.py b/backend/app/models/__init__.py index ed48d888..9bd9a65b 100644 --- a/backend/app/models/__init__.py +++ b/backend/app/models/__init__.py @@ -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", diff --git a/backend/app/models/org.py b/backend/app/models/org.py index f0d2caac..81a07c49 100644 --- a/backend/app/models/org.py +++ b/backend/app/models/org.py @@ -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 diff --git a/backend/app/models/projects.py b/backend/app/models/projects.py index acfeb15e..b079b9b2 100644 --- a/backend/app/models/projects.py +++ b/backend/app/models/projects.py @@ -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" diff --git a/backend/app/schemas/org.py b/backend/app/schemas/org.py index 102872a0..0ae1d541 100644 --- a/backend/app/schemas/org.py +++ b/backend/app/schemas/org.py @@ -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 diff --git a/backend/app/schemas/projects.py b/backend/app/schemas/projects.py index 98c33970..10477213 100644 --- a/backend/app/schemas/projects.py +++ b/backend/app/schemas/projects.py @@ -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 diff --git a/frontend/src/api/generated/model/employee.ts b/frontend/src/api/generated/model/employee.ts index 893f4eb5..5e273ee6 100644 --- a/frontend/src/api/generated/model/employee.ts +++ b/frontend/src/api/generated/model/employee.ts @@ -10,6 +10,7 @@ export interface Employee { name: string; employee_type: string; department_id?: number | null; + team_id?: number | null; manager_id?: number | null; title?: string | null; status?: string; diff --git a/frontend/src/api/generated/model/employeeCreate.ts b/frontend/src/api/generated/model/employeeCreate.ts index 74dcd459..4ca4c9ae 100644 --- a/frontend/src/api/generated/model/employeeCreate.ts +++ b/frontend/src/api/generated/model/employeeCreate.ts @@ -9,6 +9,7 @@ export interface EmployeeCreate { name: string; employee_type: string; department_id?: number | null; + team_id?: number | null; manager_id?: number | null; title?: string | null; status?: string; diff --git a/frontend/src/api/generated/model/employeeUpdate.ts b/frontend/src/api/generated/model/employeeUpdate.ts index 4cb343d3..28f03843 100644 --- a/frontend/src/api/generated/model/employeeUpdate.ts +++ b/frontend/src/api/generated/model/employeeUpdate.ts @@ -9,6 +9,7 @@ export interface EmployeeUpdate { name?: string | null; employee_type?: string | null; department_id?: number | null; + team_id?: number | null; manager_id?: number | null; title?: string | null; status?: string | null; diff --git a/frontend/src/api/generated/model/index.ts b/frontend/src/api/generated/model/index.ts index f1e08e22..f8376378 100644 --- a/frontend/src/api/generated/model/index.ts +++ b/frontend/src/api/generated/model/index.ts @@ -23,6 +23,7 @@ export * from "./hTTPValidationError"; export * from "./listActivitiesActivitiesGetParams"; export * from "./listTaskCommentsTaskCommentsGetParams"; export * from "./listTasksTasksGetParams"; +export * from "./listTeamsTeamsGetParams"; export * from "./project"; export * from "./projectCreate"; export * from "./projectMember"; @@ -32,4 +33,7 @@ export * from "./taskComment"; export * from "./taskCommentCreate"; export * from "./taskCreate"; export * from "./taskUpdate"; +export * from "./team"; +export * from "./teamCreate"; +export * from "./teamUpdate"; export * from "./validationError"; diff --git a/frontend/src/api/generated/model/listTeamsTeamsGetParams.ts b/frontend/src/api/generated/model/listTeamsTeamsGetParams.ts new file mode 100644 index 00000000..54cb6eb7 --- /dev/null +++ b/frontend/src/api/generated/model/listTeamsTeamsGetParams.ts @@ -0,0 +1,10 @@ +/** + * Generated by orval v8.2.0 🍺 + * Do not edit manually. + * OpenClaw Agency API + * OpenAPI spec version: 0.3.0 + */ + +export type ListTeamsTeamsGetParams = { + department_id?: number | null; +}; diff --git a/frontend/src/api/generated/model/project.ts b/frontend/src/api/generated/model/project.ts index 5e3c8b5d..d886a9a8 100644 --- a/frontend/src/api/generated/model/project.ts +++ b/frontend/src/api/generated/model/project.ts @@ -9,4 +9,5 @@ export interface Project { id?: number | null; name: string; status?: string; + team_id?: number | null; } diff --git a/frontend/src/api/generated/model/projectCreate.ts b/frontend/src/api/generated/model/projectCreate.ts index 01a2db54..9432eb92 100644 --- a/frontend/src/api/generated/model/projectCreate.ts +++ b/frontend/src/api/generated/model/projectCreate.ts @@ -8,4 +8,5 @@ export interface ProjectCreate { name: string; status?: string; + team_id?: number | null; } diff --git a/frontend/src/api/generated/model/projectUpdate.ts b/frontend/src/api/generated/model/projectUpdate.ts index 84ea2e92..8e6f7969 100644 --- a/frontend/src/api/generated/model/projectUpdate.ts +++ b/frontend/src/api/generated/model/projectUpdate.ts @@ -8,4 +8,5 @@ export interface ProjectUpdate { name?: string | null; status?: string | null; + team_id?: number | null; } diff --git a/frontend/src/api/generated/model/team.ts b/frontend/src/api/generated/model/team.ts new file mode 100644 index 00000000..a407a727 --- /dev/null +++ b/frontend/src/api/generated/model/team.ts @@ -0,0 +1,13 @@ +/** + * Generated by orval v8.2.0 🍺 + * Do not edit manually. + * OpenClaw Agency API + * OpenAPI spec version: 0.3.0 + */ + +export interface Team { + id?: number | null; + name: string; + department_id: number; + lead_employee_id?: number | null; +} diff --git a/frontend/src/api/generated/model/teamCreate.ts b/frontend/src/api/generated/model/teamCreate.ts new file mode 100644 index 00000000..bdf963a1 --- /dev/null +++ b/frontend/src/api/generated/model/teamCreate.ts @@ -0,0 +1,12 @@ +/** + * Generated by orval v8.2.0 🍺 + * Do not edit manually. + * OpenClaw Agency API + * OpenAPI spec version: 0.3.0 + */ + +export interface TeamCreate { + name: string; + department_id: number; + lead_employee_id?: number | null; +} diff --git a/frontend/src/api/generated/model/teamUpdate.ts b/frontend/src/api/generated/model/teamUpdate.ts new file mode 100644 index 00000000..1bfa271d --- /dev/null +++ b/frontend/src/api/generated/model/teamUpdate.ts @@ -0,0 +1,12 @@ +/** + * Generated by orval v8.2.0 🍺 + * Do not edit manually. + * OpenClaw Agency API + * OpenAPI spec version: 0.3.0 + */ + +export interface TeamUpdate { + name?: string | null; + department_id?: number | null; + lead_employee_id?: number | null; +} diff --git a/frontend/src/api/generated/org/org.ts b/frontend/src/api/generated/org/org.ts index 4c7c3f4d..23f7828a 100644 --- a/frontend/src/api/generated/org/org.ts +++ b/frontend/src/api/generated/org/org.ts @@ -28,6 +28,10 @@ import type { EmployeeCreate, EmployeeUpdate, HTTPValidationError, + ListTeamsTeamsGetParams, + Team, + TeamCreate, + TeamUpdate, } from ".././model"; import { customFetch } from "../../mutator"; @@ -327,6 +331,440 @@ export const useCreateDepartmentDepartmentsPost = < queryClient, ); }; +/** + * @summary List Teams + */ +export type listTeamsTeamsGetResponse200 = { + data: Team[]; + status: 200; +}; + +export type listTeamsTeamsGetResponse422 = { + data: HTTPValidationError; + status: 422; +}; + +export type listTeamsTeamsGetResponseSuccess = listTeamsTeamsGetResponse200 & { + headers: Headers; +}; +export type listTeamsTeamsGetResponseError = listTeamsTeamsGetResponse422 & { + headers: Headers; +}; + +export type listTeamsTeamsGetResponse = + | listTeamsTeamsGetResponseSuccess + | listTeamsTeamsGetResponseError; + +export const getListTeamsTeamsGetUrl = (params?: ListTeamsTeamsGetParams) => { + const normalizedParams = new URLSearchParams(); + + Object.entries(params || {}).forEach(([key, value]) => { + if (value !== undefined) { + normalizedParams.append(key, value === null ? "null" : value.toString()); + } + }); + + const stringifiedParams = normalizedParams.toString(); + + return stringifiedParams.length > 0 + ? `/teams?${stringifiedParams}` + : `/teams`; +}; + +export const listTeamsTeamsGet = async ( + params?: ListTeamsTeamsGetParams, + options?: RequestInit, +): Promise => { + return customFetch( + getListTeamsTeamsGetUrl(params), + { + ...options, + method: "GET", + }, + ); +}; + +export const getListTeamsTeamsGetQueryKey = ( + params?: ListTeamsTeamsGetParams, +) => { + return [`/teams`, ...(params ? [params] : [])] as const; +}; + +export const getListTeamsTeamsGetQueryOptions = < + TData = Awaited>, + TError = HTTPValidationError, +>( + params?: ListTeamsTeamsGetParams, + options?: { + query?: Partial< + UseQueryOptions< + Awaited>, + TError, + TData + > + >; + request?: SecondParameter; + }, +) => { + const { query: queryOptions, request: requestOptions } = options ?? {}; + + const queryKey = + queryOptions?.queryKey ?? getListTeamsTeamsGetQueryKey(params); + + const queryFn: QueryFunction< + Awaited> + > = ({ signal }) => listTeamsTeamsGet(params, { signal, ...requestOptions }); + + return { queryKey, queryFn, ...queryOptions } as UseQueryOptions< + Awaited>, + TError, + TData + > & { queryKey: DataTag }; +}; + +export type ListTeamsTeamsGetQueryResult = NonNullable< + Awaited> +>; +export type ListTeamsTeamsGetQueryError = HTTPValidationError; + +export function useListTeamsTeamsGet< + TData = Awaited>, + TError = HTTPValidationError, +>( + params: undefined | ListTeamsTeamsGetParams, + options: { + query: Partial< + UseQueryOptions< + Awaited>, + TError, + TData + > + > & + Pick< + DefinedInitialDataOptions< + Awaited>, + TError, + Awaited> + >, + "initialData" + >; + request?: SecondParameter; + }, + queryClient?: QueryClient, +): DefinedUseQueryResult & { + queryKey: DataTag; +}; +export function useListTeamsTeamsGet< + TData = Awaited>, + TError = HTTPValidationError, +>( + params?: ListTeamsTeamsGetParams, + options?: { + query?: Partial< + UseQueryOptions< + Awaited>, + TError, + TData + > + > & + Pick< + UndefinedInitialDataOptions< + Awaited>, + TError, + Awaited> + >, + "initialData" + >; + request?: SecondParameter; + }, + queryClient?: QueryClient, +): UseQueryResult & { + queryKey: DataTag; +}; +export function useListTeamsTeamsGet< + TData = Awaited>, + TError = HTTPValidationError, +>( + params?: ListTeamsTeamsGetParams, + options?: { + query?: Partial< + UseQueryOptions< + Awaited>, + TError, + TData + > + >; + request?: SecondParameter; + }, + queryClient?: QueryClient, +): UseQueryResult & { + queryKey: DataTag; +}; +/** + * @summary List Teams + */ + +export function useListTeamsTeamsGet< + TData = Awaited>, + TError = HTTPValidationError, +>( + params?: ListTeamsTeamsGetParams, + options?: { + query?: Partial< + UseQueryOptions< + Awaited>, + TError, + TData + > + >; + request?: SecondParameter; + }, + queryClient?: QueryClient, +): UseQueryResult & { + queryKey: DataTag; +} { + const queryOptions = getListTeamsTeamsGetQueryOptions(params, options); + + const query = useQuery(queryOptions, queryClient) as UseQueryResult< + TData, + TError + > & { queryKey: DataTag }; + + return { ...query, queryKey: queryOptions.queryKey }; +} + +/** + * @summary Create Team + */ +export type createTeamTeamsPostResponse200 = { + data: Team; + status: 200; +}; + +export type createTeamTeamsPostResponse422 = { + data: HTTPValidationError; + status: 422; +}; + +export type createTeamTeamsPostResponseSuccess = + createTeamTeamsPostResponse200 & { + headers: Headers; + }; +export type createTeamTeamsPostResponseError = + createTeamTeamsPostResponse422 & { + headers: Headers; + }; + +export type createTeamTeamsPostResponse = + | createTeamTeamsPostResponseSuccess + | createTeamTeamsPostResponseError; + +export const getCreateTeamTeamsPostUrl = () => { + return `/teams`; +}; + +export const createTeamTeamsPost = async ( + teamCreate: TeamCreate, + options?: RequestInit, +): Promise => { + return customFetch(getCreateTeamTeamsPostUrl(), { + ...options, + method: "POST", + headers: { "Content-Type": "application/json", ...options?.headers }, + body: JSON.stringify(teamCreate), + }); +}; + +export const getCreateTeamTeamsPostMutationOptions = < + TError = HTTPValidationError, + TContext = unknown, +>(options?: { + mutation?: UseMutationOptions< + Awaited>, + TError, + { data: TeamCreate }, + TContext + >; + request?: SecondParameter; +}): UseMutationOptions< + Awaited>, + TError, + { data: TeamCreate }, + TContext +> => { + const mutationKey = ["createTeamTeamsPost"]; + const { mutation: mutationOptions, request: requestOptions } = options + ? options.mutation && + "mutationKey" in options.mutation && + options.mutation.mutationKey + ? options + : { ...options, mutation: { ...options.mutation, mutationKey } } + : { mutation: { mutationKey }, request: undefined }; + + const mutationFn: MutationFunction< + Awaited>, + { data: TeamCreate } + > = (props) => { + const { data } = props ?? {}; + + return createTeamTeamsPost(data, requestOptions); + }; + + return { mutationFn, ...mutationOptions }; +}; + +export type CreateTeamTeamsPostMutationResult = NonNullable< + Awaited> +>; +export type CreateTeamTeamsPostMutationBody = TeamCreate; +export type CreateTeamTeamsPostMutationError = HTTPValidationError; + +/** + * @summary Create Team + */ +export const useCreateTeamTeamsPost = < + TError = HTTPValidationError, + TContext = unknown, +>( + options?: { + mutation?: UseMutationOptions< + Awaited>, + TError, + { data: TeamCreate }, + TContext + >; + request?: SecondParameter; + }, + queryClient?: QueryClient, +): UseMutationResult< + Awaited>, + TError, + { data: TeamCreate }, + TContext +> => { + return useMutation( + getCreateTeamTeamsPostMutationOptions(options), + queryClient, + ); +}; +/** + * @summary Update Team + */ +export type updateTeamTeamsTeamIdPatchResponse200 = { + data: Team; + status: 200; +}; + +export type updateTeamTeamsTeamIdPatchResponse422 = { + data: HTTPValidationError; + status: 422; +}; + +export type updateTeamTeamsTeamIdPatchResponseSuccess = + updateTeamTeamsTeamIdPatchResponse200 & { + headers: Headers; + }; +export type updateTeamTeamsTeamIdPatchResponseError = + updateTeamTeamsTeamIdPatchResponse422 & { + headers: Headers; + }; + +export type updateTeamTeamsTeamIdPatchResponse = + | updateTeamTeamsTeamIdPatchResponseSuccess + | updateTeamTeamsTeamIdPatchResponseError; + +export const getUpdateTeamTeamsTeamIdPatchUrl = (teamId: number) => { + return `/teams/${teamId}`; +}; + +export const updateTeamTeamsTeamIdPatch = async ( + teamId: number, + teamUpdate: TeamUpdate, + options?: RequestInit, +): Promise => { + return customFetch( + getUpdateTeamTeamsTeamIdPatchUrl(teamId), + { + ...options, + method: "PATCH", + headers: { "Content-Type": "application/json", ...options?.headers }, + body: JSON.stringify(teamUpdate), + }, + ); +}; + +export const getUpdateTeamTeamsTeamIdPatchMutationOptions = < + TError = HTTPValidationError, + TContext = unknown, +>(options?: { + mutation?: UseMutationOptions< + Awaited>, + TError, + { teamId: number; data: TeamUpdate }, + TContext + >; + request?: SecondParameter; +}): UseMutationOptions< + Awaited>, + TError, + { teamId: number; data: TeamUpdate }, + TContext +> => { + const mutationKey = ["updateTeamTeamsTeamIdPatch"]; + const { mutation: mutationOptions, request: requestOptions } = options + ? options.mutation && + "mutationKey" in options.mutation && + options.mutation.mutationKey + ? options + : { ...options, mutation: { ...options.mutation, mutationKey } } + : { mutation: { mutationKey }, request: undefined }; + + const mutationFn: MutationFunction< + Awaited>, + { teamId: number; data: TeamUpdate } + > = (props) => { + const { teamId, data } = props ?? {}; + + return updateTeamTeamsTeamIdPatch(teamId, data, requestOptions); + }; + + return { mutationFn, ...mutationOptions }; +}; + +export type UpdateTeamTeamsTeamIdPatchMutationResult = NonNullable< + Awaited> +>; +export type UpdateTeamTeamsTeamIdPatchMutationBody = TeamUpdate; +export type UpdateTeamTeamsTeamIdPatchMutationError = HTTPValidationError; + +/** + * @summary Update Team + */ +export const useUpdateTeamTeamsTeamIdPatch = < + TError = HTTPValidationError, + TContext = unknown, +>( + options?: { + mutation?: UseMutationOptions< + Awaited>, + TError, + { teamId: number; data: TeamUpdate }, + TContext + >; + request?: SecondParameter; + }, + queryClient?: QueryClient, +): UseMutationResult< + Awaited>, + TError, + { teamId: number; data: TeamUpdate }, + TContext +> => { + return useMutation( + getUpdateTeamTeamsTeamIdPatchMutationOptions(options), + queryClient, + ); +}; /** * @summary Update Department */ diff --git a/frontend/src/app/_components/Shell.tsx b/frontend/src/app/_components/Shell.tsx index 6caaa052..55b6a917 100644 --- a/frontend/src/app/_components/Shell.tsx +++ b/frontend/src/app/_components/Shell.tsx @@ -10,6 +10,7 @@ const NAV = [ { href: "/projects", label: "Projects" }, { href: "/kanban", label: "Kanban" }, { href: "/departments", label: "Departments" }, + { href: "/teams", label: "Teams" }, { href: "/people", label: "People" }, ]; diff --git a/frontend/src/app/people/page.tsx b/frontend/src/app/people/page.tsx index 96f23377..07613527 100644 --- a/frontend/src/app/people/page.tsx +++ b/frontend/src/app/people/page.tsx @@ -13,6 +13,7 @@ import { useCreateEmployeeEmployeesPost, useListDepartmentsDepartmentsGet, useListEmployeesEmployeesGet, + useListTeamsTeamsGet, } from "@/api/generated/org/org"; export default function PeoplePage() { @@ -20,12 +21,15 @@ export default function PeoplePage() { const [employeeType, setEmployeeType] = useState<"human" | "agent">("human"); const [title, setTitle] = useState(""); const [departmentId, setDepartmentId] = useState(""); + const [teamId, setTeamId] = useState(""); const [managerId, setManagerId] = useState(""); const employees = useListEmployeesEmployeesGet(); const departments = useListDepartmentsDepartmentsGet(); + const teams = useListTeamsTeamsGet({ department_id: undefined }); const departmentList = useMemo(() => (departments.data?.status === 200 ? departments.data.data : []), [departments.data]); const employeeList = useMemo(() => (employees.data?.status === 200 ? employees.data.data : []), [employees.data]); + const teamList = useMemo(() => (teams.data?.status === 200 ? teams.data.data : []), [teams.data]); const createEmployee = useCreateEmployeeEmployeesPost({ mutation: { @@ -33,6 +37,7 @@ export default function PeoplePage() { setName(""); setTitle(""); setDepartmentId(""); + setTeamId(""); setManagerId(""); employees.refetch(); }, @@ -47,6 +52,14 @@ export default function PeoplePage() { return m; }, [departmentList]); + const teamNameById = useMemo(() => { + const m = new Map(); + for (const t of teamList) { + if (t.id != null) m.set(t.id, t.name); + } + return m; + }, [teamList]); + const empNameById = useMemo(() => { const m = new Map(); for (const e of employeeList) { @@ -88,6 +101,14 @@ export default function PeoplePage() { ))} + setName(e.target.value)} autoFocus /> +
+ Owning team + +
+ + +
+ + + Create team + Define a team and attach it to a department. + + + setName(e.target.value)} /> + + + + {createTeam.error ?
{(createTeam.error as Error).message}
: null} +
+
+ + + + All teams + {sorted.length} total + + + {teams.isLoading ?
Loading…
: null} + {teams.error ?
{(teams.error as Error).message}
: null} + {!teams.isLoading && !teams.error ? ( +
    + {sorted.map((t) => ( +
  • +
    +
    {t.name}
    +
    {deptNameById.get(t.department_id) ?? `Dept#${t.department_id}`}
    +
    +
    + {t.lead_employee_id ? Lead: {empNameById.get(t.lead_employee_id) ?? `Emp#${t.lead_employee_id}`} : No lead} +
    +
  • + ))} + {sorted.length === 0 ?
  • No teams yet.
  • : null} +
+ ) : null} +
+
+
+ + ); +}