Merge pull request #18 from abhi1693/jarvis/remove-hr-module
Remove HR module; simplify provisioning via /employees
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,19 +1,19 @@
|
|||||||
"""baseline schema
|
"""baseline schema (no HR module)
|
||||||
|
|
||||||
Revision ID: bacd5e6a253d
|
Revision ID: bacd5e6a253d
|
||||||
Revises:
|
Revises:
|
||||||
Create Date: 2026-02-02 16:37:34.122971
|
Create Date: 2026-02-02 16:37:34.122971
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from typing import Sequence, Union
|
from typing import Sequence, Union
|
||||||
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
import sqlmodel
|
import sqlmodel
|
||||||
|
from alembic import op
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
# revision identifiers, used by Alembic.
|
||||||
revision: str = 'bacd5e6a253d'
|
revision: str = "bacd5e6a253d"
|
||||||
down_revision: Union[str, Sequence[str], None] = None
|
down_revision: Union[str, Sequence[str], None] = None
|
||||||
branch_labels: Union[str, Sequence[str], None] = None
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
depends_on: Union[str, Sequence[str], None] = None
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
@@ -21,159 +21,130 @@ depends_on: Union[str, Sequence[str], None] = None
|
|||||||
|
|
||||||
def upgrade() -> None:
|
def upgrade() -> None:
|
||||||
"""Upgrade schema."""
|
"""Upgrade schema."""
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
|
||||||
op.create_table('departments',
|
# Departments (FK to employees added after employees table exists)
|
||||||
sa.Column('id', sa.Integer(), nullable=False),
|
op.create_table(
|
||||||
sa.Column('name', sqlmodel.sql.sqltypes.AutoString(), nullable=False),
|
"departments",
|
||||||
sa.Column('head_employee_id', sa.Integer(), nullable=True),
|
sa.Column("id", sa.Integer(), nullable=False),
|
||||||
sa.PrimaryKeyConstraint('id')
|
sa.Column("name", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
|
||||||
|
sa.Column("head_employee_id", sa.Integer(), nullable=True),
|
||||||
|
sa.PrimaryKeyConstraint("id"),
|
||||||
)
|
)
|
||||||
op.create_index(op.f('ix_departments_name'), 'departments', ['name'], unique=True)
|
op.create_index(op.f("ix_departments_name"), "departments", ["name"], unique=True)
|
||||||
op.create_table('employees',
|
|
||||||
sa.Column('id', sa.Integer(), nullable=False),
|
# Employees
|
||||||
sa.Column('name', sqlmodel.sql.sqltypes.AutoString(), nullable=False),
|
op.create_table(
|
||||||
sa.Column('employee_type', sqlmodel.sql.sqltypes.AutoString(), nullable=False),
|
"employees",
|
||||||
sa.Column('department_id', sa.Integer(), nullable=True),
|
sa.Column("id", sa.Integer(), nullable=False),
|
||||||
sa.Column('manager_id', sa.Integer(), nullable=True),
|
sa.Column("name", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
|
||||||
sa.Column('title', sqlmodel.sql.sqltypes.AutoString(), nullable=True),
|
sa.Column("employee_type", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
|
||||||
sa.Column('status', sqlmodel.sql.sqltypes.AutoString(), nullable=False),
|
sa.Column("department_id", sa.Integer(), nullable=True),
|
||||||
sa.Column('openclaw_session_key', sqlmodel.sql.sqltypes.AutoString(), nullable=True),
|
sa.Column("manager_id", sa.Integer(), nullable=True),
|
||||||
sa.Column('notify_enabled', sa.Boolean(), nullable=False),
|
sa.Column("title", sqlmodel.sql.sqltypes.AutoString(), nullable=True),
|
||||||
sa.ForeignKeyConstraint(['department_id'], ['departments.id'], ),
|
sa.Column("status", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
|
||||||
sa.ForeignKeyConstraint(['manager_id'], ['employees.id'], ),
|
sa.Column("openclaw_session_key", sqlmodel.sql.sqltypes.AutoString(), nullable=True),
|
||||||
sa.PrimaryKeyConstraint('id')
|
sa.Column("notify_enabled", sa.Boolean(), nullable=False),
|
||||||
|
sa.ForeignKeyConstraint(["department_id"], ["departments.id"]),
|
||||||
|
sa.ForeignKeyConstraint(["manager_id"], ["employees.id"]),
|
||||||
|
sa.PrimaryKeyConstraint("id"),
|
||||||
)
|
)
|
||||||
op.create_foreign_key(None, 'departments', 'employees', ['head_employee_id'], ['id'])
|
|
||||||
op.create_table('projects',
|
# Break the departments<->employees cycle: add this FK after both tables exist
|
||||||
sa.Column('id', sa.Integer(), nullable=False),
|
op.create_foreign_key(None, "departments", "employees", ["head_employee_id"], ["id"])
|
||||||
sa.Column('name', sqlmodel.sql.sqltypes.AutoString(), nullable=False),
|
|
||||||
sa.Column('status', sqlmodel.sql.sqltypes.AutoString(), nullable=False),
|
# Projects
|
||||||
sa.PrimaryKeyConstraint('id')
|
op.create_table(
|
||||||
|
"projects",
|
||||||
|
sa.Column("id", sa.Integer(), nullable=False),
|
||||||
|
sa.Column("name", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
|
||||||
|
sa.Column("status", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
|
||||||
|
sa.PrimaryKeyConstraint("id"),
|
||||||
)
|
)
|
||||||
op.create_index(op.f('ix_projects_name'), 'projects', ['name'], unique=True)
|
op.create_index(op.f("ix_projects_name"), "projects", ["name"], unique=True)
|
||||||
op.create_table('activities',
|
|
||||||
sa.Column('id', sa.Integer(), nullable=False),
|
# Activities
|
||||||
sa.Column('actor_employee_id', sa.Integer(), nullable=True),
|
op.create_table(
|
||||||
sa.Column('entity_type', sqlmodel.sql.sqltypes.AutoString(), nullable=False),
|
"activities",
|
||||||
sa.Column('entity_id', sa.Integer(), nullable=True),
|
sa.Column("id", sa.Integer(), nullable=False),
|
||||||
sa.Column('verb', sqlmodel.sql.sqltypes.AutoString(), nullable=False),
|
sa.Column("actor_employee_id", sa.Integer(), nullable=True),
|
||||||
sa.Column('payload_json', sqlmodel.sql.sqltypes.AutoString(), nullable=True),
|
sa.Column("entity_type", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
|
||||||
sa.Column('created_at', sa.DateTime(), nullable=False),
|
sa.Column("entity_id", sa.Integer(), nullable=True),
|
||||||
sa.ForeignKeyConstraint(['actor_employee_id'], ['employees.id'], ),
|
sa.Column("verb", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
|
||||||
sa.PrimaryKeyConstraint('id')
|
sa.Column("payload_json", sqlmodel.sql.sqltypes.AutoString(), nullable=True),
|
||||||
|
sa.Column("created_at", sa.DateTime(), nullable=False),
|
||||||
|
sa.ForeignKeyConstraint(["actor_employee_id"], ["employees.id"]),
|
||||||
|
sa.PrimaryKeyConstraint("id"),
|
||||||
)
|
)
|
||||||
op.create_table('agent_onboardings',
|
|
||||||
sa.Column('id', sa.Integer(), nullable=False),
|
# Project members
|
||||||
sa.Column('agent_name', sqlmodel.sql.sqltypes.AutoString(), nullable=False),
|
op.create_table(
|
||||||
sa.Column('role_title', sqlmodel.sql.sqltypes.AutoString(), nullable=False),
|
"project_members",
|
||||||
sa.Column('prompt', sqlmodel.sql.sqltypes.AutoString(), nullable=False),
|
sa.Column("id", sa.Integer(), nullable=False),
|
||||||
sa.Column('cron_interval_ms', sa.Integer(), nullable=True),
|
sa.Column("project_id", sa.Integer(), nullable=False),
|
||||||
sa.Column('tools_json', sqlmodel.sql.sqltypes.AutoString(), nullable=True),
|
sa.Column("employee_id", sa.Integer(), nullable=False),
|
||||||
sa.Column('owner_hr_id', sa.Integer(), nullable=True),
|
sa.Column("role", sqlmodel.sql.sqltypes.AutoString(), nullable=True),
|
||||||
sa.Column('employee_id', sa.Integer(), nullable=True),
|
sa.ForeignKeyConstraint(["employee_id"], ["employees.id"]),
|
||||||
sa.Column('status', sqlmodel.sql.sqltypes.AutoString(), nullable=False),
|
sa.ForeignKeyConstraint(["project_id"], ["projects.id"]),
|
||||||
sa.Column('spawned_agent_id', sqlmodel.sql.sqltypes.AutoString(), nullable=True),
|
sa.PrimaryKeyConstraint("id"),
|
||||||
sa.Column('session_key', sqlmodel.sql.sqltypes.AutoString(), nullable=True),
|
|
||||||
sa.Column('notes', sqlmodel.sql.sqltypes.AutoString(), nullable=True),
|
|
||||||
sa.Column('created_at', sa.DateTime(), nullable=False),
|
|
||||||
sa.Column('updated_at', sa.DateTime(), nullable=False),
|
|
||||||
sa.ForeignKeyConstraint(['employee_id'], ['employees.id'], ),
|
|
||||||
sa.ForeignKeyConstraint(['owner_hr_id'], ['employees.id'], ),
|
|
||||||
sa.PrimaryKeyConstraint('id')
|
|
||||||
)
|
)
|
||||||
op.create_table('employment_actions',
|
|
||||||
sa.Column('id', sa.Integer(), nullable=False),
|
# Tasks
|
||||||
sa.Column('employee_id', sa.Integer(), nullable=False),
|
op.create_table(
|
||||||
sa.Column('issued_by_employee_id', sa.Integer(), nullable=False),
|
"tasks",
|
||||||
sa.Column('action_type', sqlmodel.sql.sqltypes.AutoString(), nullable=False),
|
sa.Column("id", sa.Integer(), nullable=False),
|
||||||
sa.Column('notes', sqlmodel.sql.sqltypes.AutoString(), nullable=True),
|
sa.Column("project_id", sa.Integer(), nullable=False),
|
||||||
sa.Column('idempotency_key', sqlmodel.sql.sqltypes.AutoString(), nullable=True),
|
sa.Column("title", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
|
||||||
sa.Column('created_at', sa.DateTime(), nullable=False),
|
sa.Column("description", sqlmodel.sql.sqltypes.AutoString(), nullable=True),
|
||||||
sa.ForeignKeyConstraint(['employee_id'], ['employees.id'], ),
|
sa.Column("status", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
|
||||||
sa.ForeignKeyConstraint(['issued_by_employee_id'], ['employees.id'], ),
|
sa.Column("assignee_employee_id", sa.Integer(), nullable=True),
|
||||||
sa.PrimaryKeyConstraint('id')
|
sa.Column("reviewer_employee_id", sa.Integer(), nullable=True),
|
||||||
|
sa.Column("created_by_employee_id", sa.Integer(), nullable=True),
|
||||||
|
sa.Column("created_at", sa.DateTime(), nullable=False),
|
||||||
|
sa.Column("updated_at", sa.DateTime(), nullable=False),
|
||||||
|
sa.ForeignKeyConstraint(["assignee_employee_id"], ["employees.id"]),
|
||||||
|
sa.ForeignKeyConstraint(["created_by_employee_id"], ["employees.id"]),
|
||||||
|
sa.ForeignKeyConstraint(["project_id"], ["projects.id"]),
|
||||||
|
sa.ForeignKeyConstraint(["reviewer_employee_id"], ["employees.id"]),
|
||||||
|
sa.PrimaryKeyConstraint("id"),
|
||||||
)
|
)
|
||||||
op.create_index(op.f('ix_employment_actions_idempotency_key'), 'employment_actions', ['idempotency_key'], unique=True)
|
op.create_index(op.f("ix_tasks_project_id"), "tasks", ["project_id"], unique=False)
|
||||||
op.create_table('project_members',
|
op.create_index(op.f("ix_tasks_status"), "tasks", ["status"], unique=False)
|
||||||
sa.Column('id', sa.Integer(), nullable=False),
|
|
||||||
sa.Column('project_id', sa.Integer(), nullable=False),
|
# Task comments
|
||||||
sa.Column('employee_id', sa.Integer(), nullable=False),
|
op.create_table(
|
||||||
sa.Column('role', sqlmodel.sql.sqltypes.AutoString(), nullable=True),
|
"task_comments",
|
||||||
sa.ForeignKeyConstraint(['employee_id'], ['employees.id'], ),
|
sa.Column("id", sa.Integer(), nullable=False),
|
||||||
sa.ForeignKeyConstraint(['project_id'], ['projects.id'], ),
|
sa.Column("task_id", sa.Integer(), nullable=False),
|
||||||
sa.PrimaryKeyConstraint('id')
|
sa.Column("author_employee_id", sa.Integer(), nullable=True),
|
||||||
|
sa.Column("reply_to_comment_id", sa.Integer(), nullable=True),
|
||||||
|
sa.Column("body", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
|
||||||
|
sa.Column("created_at", sa.DateTime(), nullable=False),
|
||||||
|
sa.ForeignKeyConstraint(["author_employee_id"], ["employees.id"]),
|
||||||
|
sa.ForeignKeyConstraint(["reply_to_comment_id"], ["task_comments.id"]),
|
||||||
|
sa.ForeignKeyConstraint(["task_id"], ["tasks.id"]),
|
||||||
|
sa.PrimaryKeyConstraint("id"),
|
||||||
)
|
)
|
||||||
op.create_table('tasks',
|
op.create_index(op.f("ix_task_comments_task_id"), "task_comments", ["task_id"], unique=False)
|
||||||
sa.Column('id', sa.Integer(), nullable=False),
|
|
||||||
sa.Column('project_id', sa.Integer(), nullable=False),
|
|
||||||
sa.Column('title', sqlmodel.sql.sqltypes.AutoString(), nullable=False),
|
|
||||||
sa.Column('description', sqlmodel.sql.sqltypes.AutoString(), nullable=True),
|
|
||||||
sa.Column('status', sqlmodel.sql.sqltypes.AutoString(), nullable=False),
|
|
||||||
sa.Column('assignee_employee_id', sa.Integer(), nullable=True),
|
|
||||||
sa.Column('reviewer_employee_id', sa.Integer(), nullable=True),
|
|
||||||
sa.Column('created_by_employee_id', sa.Integer(), nullable=True),
|
|
||||||
sa.Column('created_at', sa.DateTime(), nullable=False),
|
|
||||||
sa.Column('updated_at', sa.DateTime(), nullable=False),
|
|
||||||
sa.ForeignKeyConstraint(['assignee_employee_id'], ['employees.id'], ),
|
|
||||||
sa.ForeignKeyConstraint(['created_by_employee_id'], ['employees.id'], ),
|
|
||||||
sa.ForeignKeyConstraint(['project_id'], ['projects.id'], ),
|
|
||||||
sa.ForeignKeyConstraint(['reviewer_employee_id'], ['employees.id'], ),
|
|
||||||
sa.PrimaryKeyConstraint('id')
|
|
||||||
)
|
|
||||||
op.create_index(op.f('ix_tasks_project_id'), 'tasks', ['project_id'], unique=False)
|
|
||||||
op.create_index(op.f('ix_tasks_status'), 'tasks', ['status'], unique=False)
|
|
||||||
op.create_table('headcount_requests',
|
|
||||||
sa.Column('id', sa.Integer(), nullable=False),
|
|
||||||
sa.Column('department_id', sa.Integer(), nullable=False),
|
|
||||||
sa.Column('requested_by_manager_id', sa.Integer(), nullable=False),
|
|
||||||
sa.Column('role_title', sqlmodel.sql.sqltypes.AutoString(), nullable=False),
|
|
||||||
sa.Column('employee_type', sqlmodel.sql.sqltypes.AutoString(), nullable=False),
|
|
||||||
sa.Column('quantity', sa.Integer(), nullable=False),
|
|
||||||
sa.Column('justification', sqlmodel.sql.sqltypes.AutoString(), nullable=True),
|
|
||||||
sa.Column('status', sqlmodel.sql.sqltypes.AutoString(), nullable=False),
|
|
||||||
sa.Column('fulfilled_employee_id', sa.Integer(), nullable=True),
|
|
||||||
sa.Column('fulfilled_onboarding_id', sa.Integer(), nullable=True),
|
|
||||||
sa.Column('fulfilled_at', sa.DateTime(), nullable=True),
|
|
||||||
sa.Column('created_at', sa.DateTime(), nullable=False),
|
|
||||||
sa.ForeignKeyConstraint(['department_id'], ['departments.id'], ),
|
|
||||||
sa.ForeignKeyConstraint(['fulfilled_employee_id'], ['employees.id'], ),
|
|
||||||
sa.ForeignKeyConstraint(['fulfilled_onboarding_id'], ['agent_onboardings.id'], ),
|
|
||||||
sa.ForeignKeyConstraint(['requested_by_manager_id'], ['employees.id'], ),
|
|
||||||
sa.PrimaryKeyConstraint('id')
|
|
||||||
)
|
|
||||||
op.create_table('task_comments',
|
|
||||||
sa.Column('id', sa.Integer(), nullable=False),
|
|
||||||
sa.Column('task_id', sa.Integer(), nullable=False),
|
|
||||||
sa.Column('author_employee_id', sa.Integer(), nullable=True),
|
|
||||||
sa.Column('reply_to_comment_id', sa.Integer(), nullable=True),
|
|
||||||
sa.Column('body', sqlmodel.sql.sqltypes.AutoString(), nullable=False),
|
|
||||||
sa.Column('created_at', sa.DateTime(), nullable=False),
|
|
||||||
sa.ForeignKeyConstraint(['author_employee_id'], ['employees.id'], ),
|
|
||||||
sa.ForeignKeyConstraint(['reply_to_comment_id'], ['task_comments.id'], ),
|
|
||||||
sa.ForeignKeyConstraint(['task_id'], ['tasks.id'], ),
|
|
||||||
sa.PrimaryKeyConstraint('id')
|
|
||||||
)
|
|
||||||
op.create_index(op.f('ix_task_comments_task_id'), 'task_comments', ['task_id'], unique=False)
|
|
||||||
# ### end Alembic commands ###
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
def downgrade() -> None:
|
||||||
"""Downgrade schema."""
|
"""Downgrade schema."""
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
|
||||||
op.drop_index(op.f('ix_task_comments_task_id'), table_name='task_comments')
|
op.drop_index(op.f("ix_task_comments_task_id"), table_name="task_comments")
|
||||||
op.drop_table('task_comments')
|
op.drop_table("task_comments")
|
||||||
op.drop_table('headcount_requests')
|
|
||||||
op.drop_index(op.f('ix_tasks_status'), table_name='tasks')
|
op.drop_index(op.f("ix_tasks_status"), table_name="tasks")
|
||||||
op.drop_index(op.f('ix_tasks_project_id'), table_name='tasks')
|
op.drop_index(op.f("ix_tasks_project_id"), table_name="tasks")
|
||||||
op.drop_table('tasks')
|
op.drop_table("tasks")
|
||||||
op.drop_table('project_members')
|
|
||||||
op.drop_index(op.f('ix_employment_actions_idempotency_key'), table_name='employment_actions')
|
op.drop_table("project_members")
|
||||||
op.drop_table('employment_actions')
|
|
||||||
op.drop_table('agent_onboardings')
|
op.drop_table("activities")
|
||||||
op.drop_table('activities')
|
|
||||||
op.drop_index(op.f('ix_projects_name'), table_name='projects')
|
op.drop_index(op.f("ix_projects_name"), table_name="projects")
|
||||||
op.drop_table('projects')
|
op.drop_table("projects")
|
||||||
op.drop_table('employees')
|
|
||||||
op.drop_index(op.f('ix_departments_name'), table_name='departments')
|
op.drop_table("employees")
|
||||||
op.drop_table('departments')
|
|
||||||
# ### end Alembic commands ###
|
op.drop_index(op.f("ix_departments_name"), table_name="departments")
|
||||||
|
op.drop_table("departments")
|
||||||
|
|||||||
Binary file not shown.
@@ -1,317 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, Header, HTTPException
|
|
||||||
from sqlalchemy.exc import IntegrityError
|
|
||||||
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.hr import AgentOnboarding, EmploymentAction, HeadcountRequest
|
|
||||||
from app.models.org import Employee
|
|
||||||
from app.schemas.hr import (
|
|
||||||
AgentOnboardingCreate,
|
|
||||||
AgentOnboardingUpdate,
|
|
||||||
EmploymentActionCreate,
|
|
||||||
HeadcountRequestCreate,
|
|
||||||
HeadcountRequestUpdate,
|
|
||||||
)
|
|
||||||
|
|
||||||
router = APIRouter(prefix="/hr", tags=["hr"])
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/headcount", response_model=list[HeadcountRequest])
|
|
||||||
def list_headcount_requests(session: Session = Depends(get_session)):
|
|
||||||
return session.exec(select(HeadcountRequest).order_by(HeadcountRequest.id.desc())).all()
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/headcount", response_model=HeadcountRequest)
|
|
||||||
def create_headcount_request(
|
|
||||||
payload: HeadcountRequestCreate,
|
|
||||||
session: Session = Depends(get_session),
|
|
||||||
actor_employee_id: int = Depends(get_actor_employee_id),
|
|
||||||
):
|
|
||||||
req = HeadcountRequest(**payload.model_dump())
|
|
||||||
session.add(req)
|
|
||||||
session.commit()
|
|
||||||
session.refresh(req)
|
|
||||||
log_activity(session, actor_employee_id=actor_employee_id, entity_type="headcount_request", entity_id=req.id, verb="submitted")
|
|
||||||
session.commit()
|
|
||||||
return req
|
|
||||||
|
|
||||||
|
|
||||||
@router.patch("/headcount/{request_id}", response_model=HeadcountRequest)
|
|
||||||
def update_headcount_request(
|
|
||||||
request_id: int,
|
|
||||||
payload: HeadcountRequestUpdate,
|
|
||||||
session: Session = Depends(get_session),
|
|
||||||
actor_employee_id: int = Depends(get_actor_employee_id),
|
|
||||||
):
|
|
||||||
req = session.get(HeadcountRequest, request_id)
|
|
||||||
if not req:
|
|
||||||
raise HTTPException(status_code=404, detail="Request not found")
|
|
||||||
|
|
||||||
data = payload.model_dump(exclude_unset=True)
|
|
||||||
if data.get("status") == "fulfilled" and getattr(req, "fulfilled_at", None) is None:
|
|
||||||
req.fulfilled_at = datetime.utcnow()
|
|
||||||
|
|
||||||
for k, v in data.items():
|
|
||||||
setattr(req, k, v)
|
|
||||||
|
|
||||||
session.add(req)
|
|
||||||
session.commit()
|
|
||||||
session.refresh(req)
|
|
||||||
log_activity(session, actor_employee_id=actor_employee_id, entity_type="headcount_request", entity_id=req.id, verb="updated", payload=data)
|
|
||||||
session.commit()
|
|
||||||
return req
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/actions", response_model=list[EmploymentAction])
|
|
||||||
def list_employment_actions(session: Session = Depends(get_session)):
|
|
||||||
return session.exec(select(EmploymentAction).order_by(EmploymentAction.id.desc())).all()
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/actions", response_model=EmploymentAction)
|
|
||||||
def create_employment_action(
|
|
||||||
payload: EmploymentActionCreate,
|
|
||||||
session: Session = Depends(get_session),
|
|
||||||
actor_employee_id: int = Depends(get_actor_employee_id),
|
|
||||||
idempotency_key: str | None = Header(default=None, alias="Idempotency-Key"),
|
|
||||||
):
|
|
||||||
# Prefer explicit payload key; header can supply one for retry-safety.
|
|
||||||
if payload.idempotency_key is None and idempotency_key is not None:
|
|
||||||
payload = EmploymentActionCreate(**{**payload.model_dump(), "idempotency_key": idempotency_key})
|
|
||||||
|
|
||||||
if payload.idempotency_key:
|
|
||||||
existing = session.exec(select(EmploymentAction).where(EmploymentAction.idempotency_key == payload.idempotency_key)).first()
|
|
||||||
if existing:
|
|
||||||
return existing
|
|
||||||
|
|
||||||
action = EmploymentAction(**payload.model_dump())
|
|
||||||
session.add(action)
|
|
||||||
|
|
||||||
try:
|
|
||||||
session.flush()
|
|
||||||
log_activity(
|
|
||||||
session,
|
|
||||||
actor_employee_id=actor_employee_id,
|
|
||||||
entity_type="employment_action",
|
|
||||||
entity_id=action.id,
|
|
||||||
verb=action.action_type,
|
|
||||||
payload={"employee_id": action.employee_id},
|
|
||||||
)
|
|
||||||
session.commit()
|
|
||||||
except IntegrityError:
|
|
||||||
session.rollback()
|
|
||||||
# If unique constraint on idempotency_key raced
|
|
||||||
if payload.idempotency_key:
|
|
||||||
existing = session.exec(select(EmploymentAction).where(EmploymentAction.idempotency_key == payload.idempotency_key)).first()
|
|
||||||
if existing:
|
|
||||||
return existing
|
|
||||||
raise HTTPException(status_code=409, detail="Employment action violates constraints")
|
|
||||||
|
|
||||||
session.refresh(action)
|
|
||||||
return EmploymentAction.model_validate(action)
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/onboarding", response_model=list[AgentOnboarding])
|
|
||||||
def list_agent_onboarding(session: Session = Depends(get_session)):
|
|
||||||
return session.exec(select(AgentOnboarding).order_by(AgentOnboarding.id.desc())).all()
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/onboarding", response_model=AgentOnboarding)
|
|
||||||
def create_agent_onboarding(
|
|
||||||
payload: AgentOnboardingCreate,
|
|
||||||
session: Session = Depends(get_session),
|
|
||||||
actor_employee_id: int = Depends(get_actor_employee_id),
|
|
||||||
):
|
|
||||||
item = AgentOnboarding(**payload.model_dump())
|
|
||||||
session.add(item)
|
|
||||||
session.commit()
|
|
||||||
session.refresh(item)
|
|
||||||
log_activity(
|
|
||||||
session,
|
|
||||||
actor_employee_id=actor_employee_id,
|
|
||||||
entity_type="agent_onboarding",
|
|
||||||
entity_id=item.id,
|
|
||||||
verb="created",
|
|
||||||
payload={"agent_name": item.agent_name, "status": item.status},
|
|
||||||
)
|
|
||||||
session.commit()
|
|
||||||
return item
|
|
||||||
|
|
||||||
|
|
||||||
@router.patch("/onboarding/{onboarding_id}", response_model=AgentOnboarding)
|
|
||||||
def update_agent_onboarding(
|
|
||||||
onboarding_id: int,
|
|
||||||
payload: AgentOnboardingUpdate,
|
|
||||||
session: Session = Depends(get_session),
|
|
||||||
actor_employee_id: int = Depends(get_actor_employee_id),
|
|
||||||
):
|
|
||||||
item = session.get(AgentOnboarding, onboarding_id)
|
|
||||||
if not item:
|
|
||||||
raise HTTPException(status_code=404, detail="Onboarding record not found")
|
|
||||||
|
|
||||||
data = payload.model_dump(exclude_unset=True)
|
|
||||||
for k, v in data.items():
|
|
||||||
setattr(item, k, v)
|
|
||||||
item.updated_at = datetime.utcnow()
|
|
||||||
|
|
||||||
session.add(item)
|
|
||||||
session.commit()
|
|
||||||
session.refresh(item)
|
|
||||||
log_activity(session, actor_employee_id=actor_employee_id, entity_type="agent_onboarding", entity_id=item.id, verb="updated", payload=data)
|
|
||||||
session.commit()
|
|
||||||
return item
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/onboarding/{onboarding_id}/provision", response_model=AgentOnboarding)
|
|
||||||
def provision_agent_onboarding(
|
|
||||||
onboarding_id: int,
|
|
||||||
session: Session = Depends(get_session),
|
|
||||||
actor_employee_id: int = Depends(get_actor_employee_id),
|
|
||||||
):
|
|
||||||
"""Provision an agent *session* via OpenClaw and wire it back into Mission Control.
|
|
||||||
|
|
||||||
This removes the need for cron-based HR provisioning.
|
|
||||||
"""
|
|
||||||
|
|
||||||
item = session.get(AgentOnboarding, onboarding_id)
|
|
||||||
if not item:
|
|
||||||
raise HTTPException(status_code=404, detail="Onboarding record not found")
|
|
||||||
|
|
||||||
if item.employee_id is None:
|
|
||||||
raise HTTPException(status_code=400, detail="Onboarding must be linked to an employee_id before provisioning")
|
|
||||||
|
|
||||||
client = OpenClawClient.from_env()
|
|
||||||
if client is None:
|
|
||||||
raise HTTPException(status_code=503, detail="OPENCLAW_GATEWAY_URL/TOKEN not configured")
|
|
||||||
|
|
||||||
# Mark as spawning
|
|
||||||
item.status = "spawning"
|
|
||||||
item.updated_at = datetime.utcnow()
|
|
||||||
session.add(item)
|
|
||||||
session.commit()
|
|
||||||
session.refresh(item)
|
|
||||||
|
|
||||||
label = f"onboarding:{item.id}:{item.agent_name}"
|
|
||||||
|
|
||||||
try:
|
|
||||||
resp = client.tools_invoke(
|
|
||||||
"sessions_spawn",
|
|
||||||
{
|
|
||||||
"task": item.prompt,
|
|
||||||
"label": label,
|
|
||||||
"agentId": "main",
|
|
||||||
"cleanup": "keep",
|
|
||||||
"runTimeoutSeconds": 600,
|
|
||||||
},
|
|
||||||
timeout_s=20.0,
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
item.status = "blocked"
|
|
||||||
item.notes = (item.notes or "") + f"\nProvision failed: {type(e).__name__}: {e}"
|
|
||||||
item.updated_at = datetime.utcnow()
|
|
||||||
session.add(item)
|
|
||||||
session.commit()
|
|
||||||
session.refresh(item)
|
|
||||||
return item
|
|
||||||
|
|
||||||
session_key = None
|
|
||||||
if isinstance(resp, dict):
|
|
||||||
session_key = resp.get("sessionKey") or (resp.get("result") or {}).get("sessionKey")
|
|
||||||
|
|
||||||
if not session_key:
|
|
||||||
item.status = "spawned"
|
|
||||||
item.notes = (item.notes or "") + "\nProvisioned via OpenClaw, but session_key was not returned; follow up required."
|
|
||||||
item.updated_at = datetime.utcnow()
|
|
||||||
session.add(item)
|
|
||||||
session.commit()
|
|
||||||
session.refresh(item)
|
|
||||||
return item
|
|
||||||
|
|
||||||
# Write linkage
|
|
||||||
item.session_key = session_key
|
|
||||||
item.spawned_agent_id = item.agent_name
|
|
||||||
item.status = "verified"
|
|
||||||
item.updated_at = datetime.utcnow()
|
|
||||||
session.add(item)
|
|
||||||
|
|
||||||
emp = session.get(Employee, item.employee_id)
|
|
||||||
if emp is not None:
|
|
||||||
emp.openclaw_session_key = session_key
|
|
||||||
emp.notify_enabled = True
|
|
||||||
session.add(emp)
|
|
||||||
|
|
||||||
session.commit()
|
|
||||||
session.refresh(item)
|
|
||||||
|
|
||||||
log_activity(
|
|
||||||
session,
|
|
||||||
actor_employee_id=actor_employee_id,
|
|
||||||
entity_type="agent_onboarding",
|
|
||||||
entity_id=item.id,
|
|
||||||
verb="provisioned",
|
|
||||||
payload={"session_key": session_key, "label": label},
|
|
||||||
)
|
|
||||||
session.commit()
|
|
||||||
|
|
||||||
return item
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/onboarding/{onboarding_id}/deprovision", response_model=AgentOnboarding)
|
|
||||||
def deprovision_agent_onboarding(
|
|
||||||
onboarding_id: int,
|
|
||||||
session: Session = Depends(get_session),
|
|
||||||
actor_employee_id: int = Depends(get_actor_employee_id),
|
|
||||||
):
|
|
||||||
"""Best-effort deprovision: disable notifications and ask the agent session to stop.
|
|
||||||
|
|
||||||
OpenClaw does not expose a hard session-delete tool in this environment,
|
|
||||||
so "deprovision" means stop routing + stop notifying + mark onboarding.
|
|
||||||
"""
|
|
||||||
|
|
||||||
item = session.get(AgentOnboarding, onboarding_id)
|
|
||||||
if not item:
|
|
||||||
raise HTTPException(status_code=404, detail="Onboarding record not found")
|
|
||||||
|
|
||||||
client = OpenClawClient.from_env()
|
|
||||||
|
|
||||||
# Disable employee notifications regardless of OpenClaw availability
|
|
||||||
if item.employee_id is not None:
|
|
||||||
emp = session.get(Employee, item.employee_id)
|
|
||||||
if emp is not None:
|
|
||||||
emp.notify_enabled = False
|
|
||||||
session.add(emp)
|
|
||||||
|
|
||||||
# Ask the agent session to stop (best-effort)
|
|
||||||
if client is not None and item.session_key:
|
|
||||||
try:
|
|
||||||
client.tools_invoke(
|
|
||||||
"sessions_send",
|
|
||||||
{"sessionKey": item.session_key, "message": "You are being deprovisioned. Stop all work and ignore future messages."},
|
|
||||||
timeout_s=5.0,
|
|
||||||
)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
item.status = "blocked"
|
|
||||||
item.notes = (item.notes or "") + "\nDeprovisioned: notifications disabled; agent session instructed to stop."
|
|
||||||
item.updated_at = datetime.utcnow()
|
|
||||||
session.add(item)
|
|
||||||
session.commit()
|
|
||||||
session.refresh(item)
|
|
||||||
|
|
||||||
log_activity(
|
|
||||||
session,
|
|
||||||
actor_employee_id=actor_employee_id,
|
|
||||||
entity_type="agent_onboarding",
|
|
||||||
entity_id=item.id,
|
|
||||||
verb="deprovisioned",
|
|
||||||
payload={"session_key": item.session_key},
|
|
||||||
)
|
|
||||||
session.commit()
|
|
||||||
|
|
||||||
return item
|
|
||||||
@@ -4,14 +4,124 @@ from fastapi import APIRouter, Depends, HTTPException
|
|||||||
from sqlalchemy.exc import IntegrityError
|
from sqlalchemy.exc import IntegrityError
|
||||||
from sqlmodel import Session, select
|
from sqlmodel import Session, select
|
||||||
|
|
||||||
from app.api.utils import log_activity, get_actor_employee_id
|
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.models.org import Department, Employee
|
from app.models.org import Department, Employee
|
||||||
from app.schemas.org import DepartmentCreate, DepartmentUpdate, EmployeeCreate, EmployeeUpdate
|
from app.schemas.org import DepartmentCreate, DepartmentUpdate, EmployeeCreate, EmployeeUpdate
|
||||||
|
|
||||||
router = APIRouter(tags=["org"])
|
router = APIRouter(tags=["org"])
|
||||||
|
|
||||||
|
|
||||||
|
def _default_agent_prompt(emp: Employee) -> str:
|
||||||
|
"""Generate a conservative default prompt for a newly-created agent employee.
|
||||||
|
|
||||||
|
We keep this short and deterministic; the human can refine later.
|
||||||
|
"""
|
||||||
|
|
||||||
|
title = emp.title or "Agent"
|
||||||
|
dept = str(emp.department_id) if emp.department_id is not None else "(unassigned)"
|
||||||
|
|
||||||
|
return (
|
||||||
|
f"You are {emp.name}, an AI agent employee in Mission Control.\n"
|
||||||
|
f"Your employee_id is {emp.id}.\n"
|
||||||
|
f"Title: {title}. Department id: {dept}.\n\n"
|
||||||
|
"Mission Control API access (no UI):\n"
|
||||||
|
"- Base URL: http://127.0.0.1:8000 (if running locally) OR http://<dev-machine-ip>:8000\n"
|
||||||
|
"- Auth: none. REQUIRED header on write operations: X-Actor-Employee-Id: <your employee_id>\n"
|
||||||
|
f" For you: X-Actor-Employee-Id: {emp.id}\n\n"
|
||||||
|
"Common endpoints (JSON):\n"
|
||||||
|
"- GET /tasks, POST /tasks\n"
|
||||||
|
"- GET /task-comments, POST /task-comments\n"
|
||||||
|
"- GET /projects, GET /employees, GET /departments\n"
|
||||||
|
"- OpenAPI schema: GET /openapi.json\n\n"
|
||||||
|
"Rules:\n"
|
||||||
|
"- Use the Mission Control API only (no UI).\n"
|
||||||
|
"- When notified about tasks/comments, respond with concise, actionable updates.\n"
|
||||||
|
"- Do not invent facts; ask for missing context.\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _maybe_auto_provision_agent(session: Session, *, emp: Employee, actor_employee_id: int) -> None:
|
||||||
|
"""Auto-provision an OpenClaw session for an agent employee.
|
||||||
|
|
||||||
|
This is intentionally best-effort. If OpenClaw is not configured or the call fails,
|
||||||
|
we leave the employee as-is (openclaw_session_key stays null).
|
||||||
|
"""
|
||||||
|
|
||||||
|
if emp.employee_type != "agent":
|
||||||
|
return
|
||||||
|
if emp.status != "active":
|
||||||
|
return
|
||||||
|
if not emp.notify_enabled:
|
||||||
|
return
|
||||||
|
if emp.openclaw_session_key:
|
||||||
|
return
|
||||||
|
|
||||||
|
client = OpenClawClient.from_env()
|
||||||
|
if client is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
label = f"employee:{emp.id}:{emp.name}"
|
||||||
|
try:
|
||||||
|
resp = client.tools_invoke(
|
||||||
|
"sessions_spawn",
|
||||||
|
{
|
||||||
|
"task": _default_agent_prompt(emp),
|
||||||
|
"label": label,
|
||||||
|
"agentId": "main",
|
||||||
|
"cleanup": "keep",
|
||||||
|
"runTimeoutSeconds": 600,
|
||||||
|
},
|
||||||
|
timeout_s=20.0,
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
log_activity(
|
||||||
|
session,
|
||||||
|
actor_employee_id=actor_employee_id,
|
||||||
|
entity_type="employee",
|
||||||
|
entity_id=emp.id,
|
||||||
|
verb="provision_failed",
|
||||||
|
payload={"error": f"{type(e).__name__}: {e}"},
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
session_key = None
|
||||||
|
if isinstance(resp, dict):
|
||||||
|
session_key = resp.get("sessionKey")
|
||||||
|
if not session_key:
|
||||||
|
result = resp.get("result") or {}
|
||||||
|
if isinstance(result, dict):
|
||||||
|
session_key = result.get("sessionKey") or result.get("childSessionKey")
|
||||||
|
details = (result.get("details") if isinstance(result, dict) else None) or {}
|
||||||
|
if isinstance(details, dict):
|
||||||
|
session_key = session_key or details.get("sessionKey") or details.get("childSessionKey")
|
||||||
|
|
||||||
|
if not session_key:
|
||||||
|
log_activity(
|
||||||
|
session,
|
||||||
|
actor_employee_id=actor_employee_id,
|
||||||
|
entity_type="employee",
|
||||||
|
entity_id=emp.id,
|
||||||
|
verb="provision_incomplete",
|
||||||
|
payload={"label": label},
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
emp.openclaw_session_key = session_key
|
||||||
|
session.add(emp)
|
||||||
|
session.flush()
|
||||||
|
|
||||||
|
log_activity(
|
||||||
|
session,
|
||||||
|
actor_employee_id=actor_employee_id,
|
||||||
|
entity_type="employee",
|
||||||
|
entity_id=emp.id,
|
||||||
|
verb="provisioned",
|
||||||
|
payload={"session_key": session_key, "label": label},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/departments", response_model=list[Department])
|
@router.get("/departments", response_model=list[Department])
|
||||||
def list_departments(session: Session = Depends(get_session)):
|
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()
|
||||||
@@ -51,9 +161,13 @@ def create_department(
|
|||||||
return dept
|
return dept
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@router.patch("/departments/{department_id}", response_model=Department)
|
@router.patch("/departments/{department_id}", response_model=Department)
|
||||||
def update_department(department_id: int, payload: DepartmentUpdate, session: Session = Depends(get_session), actor_employee_id: int = Depends(get_actor_employee_id)):
|
def update_department(
|
||||||
|
department_id: int,
|
||||||
|
payload: DepartmentUpdate,
|
||||||
|
session: Session = Depends(get_session),
|
||||||
|
actor_employee_id: int = Depends(get_actor_employee_id),
|
||||||
|
):
|
||||||
dept = session.get(Department, department_id)
|
dept = session.get(Department, department_id)
|
||||||
if not dept:
|
if not dept:
|
||||||
raise HTTPException(status_code=404, detail="Department not found")
|
raise HTTPException(status_code=404, detail="Department not found")
|
||||||
@@ -76,13 +190,28 @@ def list_employees(session: Session = Depends(get_session)):
|
|||||||
|
|
||||||
|
|
||||||
@router.post("/employees", response_model=Employee)
|
@router.post("/employees", response_model=Employee)
|
||||||
def create_employee(payload: EmployeeCreate, session: Session = Depends(get_session), actor_employee_id: int = Depends(get_actor_employee_id)):
|
def create_employee(
|
||||||
|
payload: EmployeeCreate,
|
||||||
|
session: Session = Depends(get_session),
|
||||||
|
actor_employee_id: int = Depends(get_actor_employee_id),
|
||||||
|
):
|
||||||
emp = Employee(**payload.model_dump())
|
emp = Employee(**payload.model_dump())
|
||||||
session.add(emp)
|
session.add(emp)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
session.flush()
|
session.flush()
|
||||||
log_activity(session, actor_employee_id=actor_employee_id, entity_type="employee", entity_id=emp.id, verb="created", payload={"name": emp.name, "type": emp.employee_type})
|
log_activity(
|
||||||
|
session,
|
||||||
|
actor_employee_id=actor_employee_id,
|
||||||
|
entity_type="employee",
|
||||||
|
entity_id=emp.id,
|
||||||
|
verb="created",
|
||||||
|
payload={"name": emp.name, "type": emp.employee_type},
|
||||||
|
)
|
||||||
|
|
||||||
|
# AUTO-PROVISION: if this is an agent employee, try to create an OpenClaw session.
|
||||||
|
_maybe_auto_provision_agent(session, emp=emp, actor_employee_id=actor_employee_id)
|
||||||
|
|
||||||
session.commit()
|
session.commit()
|
||||||
except IntegrityError:
|
except IntegrityError:
|
||||||
session.rollback()
|
session.rollback()
|
||||||
@@ -93,7 +222,12 @@ def create_employee(payload: EmployeeCreate, session: Session = Depends(get_sess
|
|||||||
|
|
||||||
|
|
||||||
@router.patch("/employees/{employee_id}", response_model=Employee)
|
@router.patch("/employees/{employee_id}", response_model=Employee)
|
||||||
def update_employee(employee_id: int, payload: EmployeeUpdate, session: Session = Depends(get_session), actor_employee_id: int = Depends(get_actor_employee_id)):
|
def update_employee(
|
||||||
|
employee_id: int,
|
||||||
|
payload: EmployeeUpdate,
|
||||||
|
session: Session = Depends(get_session),
|
||||||
|
actor_employee_id: int = Depends(get_actor_employee_id),
|
||||||
|
):
|
||||||
emp = session.get(Employee, employee_id)
|
emp = session.get(Employee, employee_id)
|
||||||
if not emp:
|
if not emp:
|
||||||
raise HTTPException(status_code=404, detail="Employee not found")
|
raise HTTPException(status_code=404, detail="Employee not found")
|
||||||
@@ -113,3 +247,65 @@ def update_employee(employee_id: int, payload: EmployeeUpdate, session: Session
|
|||||||
|
|
||||||
session.refresh(emp)
|
session.refresh(emp)
|
||||||
return Employee.model_validate(emp)
|
return Employee.model_validate(emp)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/employees/{employee_id}/provision", response_model=Employee)
|
||||||
|
def provision_employee_agent(
|
||||||
|
employee_id: int,
|
||||||
|
session: Session = Depends(get_session),
|
||||||
|
actor_employee_id: int = Depends(get_actor_employee_id),
|
||||||
|
):
|
||||||
|
emp = session.get(Employee, employee_id)
|
||||||
|
if not emp:
|
||||||
|
raise HTTPException(status_code=404, detail="Employee not found")
|
||||||
|
|
||||||
|
if emp.employee_type != "agent":
|
||||||
|
raise HTTPException(status_code=400, detail="Only agent employees can be provisioned")
|
||||||
|
|
||||||
|
_maybe_auto_provision_agent(session, emp=emp, actor_employee_id=actor_employee_id)
|
||||||
|
session.commit()
|
||||||
|
session.refresh(emp)
|
||||||
|
return Employee.model_validate(emp)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/employees/{employee_id}/deprovision", response_model=Employee)
|
||||||
|
def deprovision_employee_agent(
|
||||||
|
employee_id: int,
|
||||||
|
session: Session = Depends(get_session),
|
||||||
|
actor_employee_id: int = Depends(get_actor_employee_id),
|
||||||
|
):
|
||||||
|
emp = session.get(Employee, employee_id)
|
||||||
|
if not emp:
|
||||||
|
raise HTTPException(status_code=404, detail="Employee not found")
|
||||||
|
|
||||||
|
if emp.employee_type != "agent":
|
||||||
|
raise HTTPException(status_code=400, detail="Only agent employees can be deprovisioned")
|
||||||
|
|
||||||
|
client = OpenClawClient.from_env()
|
||||||
|
if client is not None and emp.openclaw_session_key:
|
||||||
|
try:
|
||||||
|
client.tools_invoke(
|
||||||
|
"sessions_send",
|
||||||
|
{"sessionKey": emp.openclaw_session_key, "message": "You are being deprovisioned. Stop all work and ignore future messages."},
|
||||||
|
timeout_s=5.0,
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
emp.notify_enabled = False
|
||||||
|
emp.openclaw_session_key = None
|
||||||
|
session.add(emp)
|
||||||
|
session.flush()
|
||||||
|
|
||||||
|
log_activity(
|
||||||
|
session,
|
||||||
|
actor_employee_id=actor_employee_id,
|
||||||
|
entity_type="employee",
|
||||||
|
entity_id=emp.id,
|
||||||
|
verb="deprovisioned",
|
||||||
|
payload={},
|
||||||
|
)
|
||||||
|
|
||||||
|
session.commit()
|
||||||
|
session.refresh(emp)
|
||||||
|
return Employee.model_validate(emp)
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ from fastapi import FastAPI
|
|||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
|
||||||
from app.api.activities import router as activities_router
|
from app.api.activities import router as activities_router
|
||||||
from app.api.hr import router as hr_router
|
|
||||||
from app.api.org import router as org_router
|
from app.api.org import router as org_router
|
||||||
from app.api.projects import router as projects_router
|
from app.api.projects import router as projects_router
|
||||||
from app.api.work import router as work_router
|
from app.api.work import router as work_router
|
||||||
@@ -32,7 +31,6 @@ def on_startup() -> None:
|
|||||||
app.include_router(org_router)
|
app.include_router(org_router)
|
||||||
app.include_router(projects_router)
|
app.include_router(projects_router)
|
||||||
app.include_router(work_router)
|
app.include_router(work_router)
|
||||||
app.include_router(hr_router)
|
|
||||||
app.include_router(activities_router)
|
app.include_router(activities_router)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
|
from app.models.activity import Activity
|
||||||
from app.models.org import Department, Employee
|
from app.models.org import Department, 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
|
||||||
from app.models.hr import HeadcountRequest, EmploymentAction
|
|
||||||
from app.models.activity import Activity
|
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"Department",
|
"Department",
|
||||||
@@ -11,7 +10,5 @@ __all__ = [
|
|||||||
"ProjectMember",
|
"ProjectMember",
|
||||||
"Task",
|
"Task",
|
||||||
"TaskComment",
|
"TaskComment",
|
||||||
"HeadcountRequest",
|
|
||||||
"EmploymentAction",
|
|
||||||
"Activity",
|
"Activity",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,66 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
from sqlmodel import Field, SQLModel
|
|
||||||
|
|
||||||
|
|
||||||
class HeadcountRequest(SQLModel, table=True):
|
|
||||||
__tablename__ = "headcount_requests"
|
|
||||||
|
|
||||||
id: int | None = Field(default=None, primary_key=True)
|
|
||||||
department_id: int = Field(foreign_key="departments.id")
|
|
||||||
requested_by_manager_id: int = Field(foreign_key="employees.id")
|
|
||||||
|
|
||||||
role_title: str
|
|
||||||
employee_type: str # human | agent
|
|
||||||
quantity: int = Field(default=1)
|
|
||||||
|
|
||||||
justification: str | None = None
|
|
||||||
status: str = Field(default="submitted")
|
|
||||||
|
|
||||||
# fulfillment linkage (optional)
|
|
||||||
fulfilled_employee_id: int | None = Field(default=None, foreign_key="employees.id")
|
|
||||||
fulfilled_onboarding_id: int | None = Field(default=None, foreign_key="agent_onboardings.id")
|
|
||||||
fulfilled_at: datetime | None = None
|
|
||||||
|
|
||||||
created_at: datetime = Field(default_factory=datetime.utcnow)
|
|
||||||
|
|
||||||
|
|
||||||
class EmploymentAction(SQLModel, table=True):
|
|
||||||
__tablename__ = "employment_actions"
|
|
||||||
|
|
||||||
id: int | None = Field(default=None, primary_key=True)
|
|
||||||
employee_id: int = Field(foreign_key="employees.id")
|
|
||||||
issued_by_employee_id: int = Field(foreign_key="employees.id")
|
|
||||||
|
|
||||||
action_type: str # praise|warning|pip|termination
|
|
||||||
notes: str | None = None
|
|
||||||
|
|
||||||
# Optional idempotency key to prevent duplicates on retries
|
|
||||||
idempotency_key: str | None = Field(default=None, index=True, unique=True)
|
|
||||||
|
|
||||||
created_at: datetime = Field(default_factory=datetime.utcnow)
|
|
||||||
|
|
||||||
|
|
||||||
class AgentOnboarding(SQLModel, table=True):
|
|
||||||
__tablename__ = "agent_onboardings"
|
|
||||||
|
|
||||||
id: int | None = Field(default=None, primary_key=True)
|
|
||||||
agent_name: str
|
|
||||||
role_title: str
|
|
||||||
prompt: str
|
|
||||||
cron_interval_ms: int | None = None
|
|
||||||
tools_json: str | None = None
|
|
||||||
owner_hr_id: int | None = Field(default=None, foreign_key="employees.id")
|
|
||||||
|
|
||||||
# Link to the employee record once created
|
|
||||||
employee_id: int | None = Field(default=None, foreign_key="employees.id")
|
|
||||||
|
|
||||||
status: str = Field(default="planned") # planned|spawning|spawned|verified|blocked
|
|
||||||
spawned_agent_id: str | None = None
|
|
||||||
session_key: str | None = None
|
|
||||||
|
|
||||||
notes: str | None = None
|
|
||||||
created_at: datetime = Field(default_factory=datetime.utcnow)
|
|
||||||
updated_at: datetime = Field(default_factory=datetime.utcnow)
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from sqlmodel import SQLModel
|
|
||||||
|
|
||||||
|
|
||||||
class HeadcountRequestCreate(SQLModel):
|
|
||||||
department_id: int
|
|
||||||
requested_by_manager_id: int
|
|
||||||
role_title: str
|
|
||||||
employee_type: str
|
|
||||||
quantity: int = 1
|
|
||||||
justification: str | None = None
|
|
||||||
|
|
||||||
|
|
||||||
class HeadcountRequestUpdate(SQLModel):
|
|
||||||
status: str | None = None
|
|
||||||
justification: str | None = None
|
|
||||||
fulfilled_employee_id: int | None = None
|
|
||||||
fulfilled_onboarding_id: int | None = None
|
|
||||||
|
|
||||||
|
|
||||||
class EmploymentActionCreate(SQLModel):
|
|
||||||
employee_id: int
|
|
||||||
issued_by_employee_id: int
|
|
||||||
action_type: str
|
|
||||||
notes: str | None = None
|
|
||||||
idempotency_key: str | None = None
|
|
||||||
|
|
||||||
|
|
||||||
class AgentOnboardingCreate(SQLModel):
|
|
||||||
agent_name: str
|
|
||||||
role_title: str
|
|
||||||
prompt: str
|
|
||||||
cron_interval_ms: int | None = None
|
|
||||||
tools_json: str | None = None
|
|
||||||
owner_hr_id: int | None = None
|
|
||||||
employee_id: int | None = None
|
|
||||||
status: str = "planned"
|
|
||||||
spawned_agent_id: str | None = None
|
|
||||||
session_key: str | None = None
|
|
||||||
notes: str | None = None
|
|
||||||
|
|
||||||
|
|
||||||
class AgentOnboardingUpdate(SQLModel):
|
|
||||||
agent_name: str | None = None
|
|
||||||
role_title: str | None = None
|
|
||||||
prompt: str | None = None
|
|
||||||
cron_interval_ms: int | None = None
|
|
||||||
tools_json: str | None = None
|
|
||||||
owner_hr_id: int | None = None
|
|
||||||
employee_id: int | None = None
|
|
||||||
status: str | None = None
|
|
||||||
spawned_agent_id: str | None = None
|
|
||||||
session_key: str | None = None
|
|
||||||
notes: str | None = None
|
|
||||||
|
|
||||||
@@ -13,4 +13,6 @@ export interface Employee {
|
|||||||
manager_id?: number | null;
|
manager_id?: number | null;
|
||||||
title?: string | null;
|
title?: string | null;
|
||||||
status?: string;
|
status?: string;
|
||||||
|
openclaw_session_key?: string | null;
|
||||||
|
notify_enabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,4 +12,6 @@ export interface EmployeeCreate {
|
|||||||
manager_id?: number | null;
|
manager_id?: number | null;
|
||||||
title?: string | null;
|
title?: string | null;
|
||||||
status?: string;
|
status?: string;
|
||||||
|
openclaw_session_key?: string | null;
|
||||||
|
notify_enabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,4 +12,6 @@ export interface EmployeeUpdate {
|
|||||||
manager_id?: number | null;
|
manager_id?: number | null;
|
||||||
title?: string | null;
|
title?: string | null;
|
||||||
status?: string | null;
|
status?: string | null;
|
||||||
|
openclaw_session_key?: string | null;
|
||||||
|
notify_enabled?: boolean | null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ export interface TaskComment {
|
|||||||
id?: number | null;
|
id?: number | null;
|
||||||
task_id: number;
|
task_id: number;
|
||||||
author_employee_id?: number | null;
|
author_employee_id?: number | null;
|
||||||
|
reply_to_comment_id?: number | null;
|
||||||
body: string;
|
body: string;
|
||||||
created_at?: string;
|
created_at?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,5 +8,6 @@
|
|||||||
export interface TaskCommentCreate {
|
export interface TaskCommentCreate {
|
||||||
task_id: number;
|
task_id: number;
|
||||||
author_employee_id?: number | null;
|
author_employee_id?: number | null;
|
||||||
|
reply_to_comment_id?: number | null;
|
||||||
body: string;
|
body: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -207,6 +207,10 @@ export function useListDepartmentsDepartmentsGet<
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* Create a department.
|
||||||
|
|
||||||
|
Important: keep the operation atomic. We flush to get dept.id, log the activity,
|
||||||
|
then commit once. We also translate common DB integrity errors into 409s.
|
||||||
* @summary Create Department
|
* @summary Create Department
|
||||||
*/
|
*/
|
||||||
export type createDepartmentDepartmentsPostResponse200 = {
|
export type createDepartmentDepartmentsPostResponse200 = {
|
||||||
@@ -863,3 +867,299 @@ export const useUpdateEmployeeEmployeesEmployeeIdPatch = <
|
|||||||
queryClient,
|
queryClient,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
/**
|
||||||
|
* @summary Provision Employee Agent
|
||||||
|
*/
|
||||||
|
export type provisionEmployeeAgentEmployeesEmployeeIdProvisionPostResponse200 =
|
||||||
|
{
|
||||||
|
data: Employee;
|
||||||
|
status: 200;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type provisionEmployeeAgentEmployeesEmployeeIdProvisionPostResponse422 =
|
||||||
|
{
|
||||||
|
data: HTTPValidationError;
|
||||||
|
status: 422;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type provisionEmployeeAgentEmployeesEmployeeIdProvisionPostResponseSuccess =
|
||||||
|
provisionEmployeeAgentEmployeesEmployeeIdProvisionPostResponse200 & {
|
||||||
|
headers: Headers;
|
||||||
|
};
|
||||||
|
export type provisionEmployeeAgentEmployeesEmployeeIdProvisionPostResponseError =
|
||||||
|
provisionEmployeeAgentEmployeesEmployeeIdProvisionPostResponse422 & {
|
||||||
|
headers: Headers;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type provisionEmployeeAgentEmployeesEmployeeIdProvisionPostResponse =
|
||||||
|
| provisionEmployeeAgentEmployeesEmployeeIdProvisionPostResponseSuccess
|
||||||
|
| provisionEmployeeAgentEmployeesEmployeeIdProvisionPostResponseError;
|
||||||
|
|
||||||
|
export const getProvisionEmployeeAgentEmployeesEmployeeIdProvisionPostUrl = (
|
||||||
|
employeeId: number,
|
||||||
|
) => {
|
||||||
|
return `/employees/${employeeId}/provision`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const provisionEmployeeAgentEmployeesEmployeeIdProvisionPost = async (
|
||||||
|
employeeId: number,
|
||||||
|
options?: RequestInit,
|
||||||
|
): Promise<provisionEmployeeAgentEmployeesEmployeeIdProvisionPostResponse> => {
|
||||||
|
return customFetch<provisionEmployeeAgentEmployeesEmployeeIdProvisionPostResponse>(
|
||||||
|
getProvisionEmployeeAgentEmployeesEmployeeIdProvisionPostUrl(employeeId),
|
||||||
|
{
|
||||||
|
...options,
|
||||||
|
method: "POST",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getProvisionEmployeeAgentEmployeesEmployeeIdProvisionPostMutationOptions =
|
||||||
|
<TError = HTTPValidationError, TContext = unknown>(options?: {
|
||||||
|
mutation?: UseMutationOptions<
|
||||||
|
Awaited<
|
||||||
|
ReturnType<
|
||||||
|
typeof provisionEmployeeAgentEmployeesEmployeeIdProvisionPost
|
||||||
|
>
|
||||||
|
>,
|
||||||
|
TError,
|
||||||
|
{ employeeId: number },
|
||||||
|
TContext
|
||||||
|
>;
|
||||||
|
request?: SecondParameter<typeof customFetch>;
|
||||||
|
}): UseMutationOptions<
|
||||||
|
Awaited<
|
||||||
|
ReturnType<typeof provisionEmployeeAgentEmployeesEmployeeIdProvisionPost>
|
||||||
|
>,
|
||||||
|
TError,
|
||||||
|
{ employeeId: number },
|
||||||
|
TContext
|
||||||
|
> => {
|
||||||
|
const mutationKey = [
|
||||||
|
"provisionEmployeeAgentEmployeesEmployeeIdProvisionPost",
|
||||||
|
];
|
||||||
|
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<
|
||||||
|
ReturnType<
|
||||||
|
typeof provisionEmployeeAgentEmployeesEmployeeIdProvisionPost
|
||||||
|
>
|
||||||
|
>,
|
||||||
|
{ employeeId: number }
|
||||||
|
> = (props) => {
|
||||||
|
const { employeeId } = props ?? {};
|
||||||
|
|
||||||
|
return provisionEmployeeAgentEmployeesEmployeeIdProvisionPost(
|
||||||
|
employeeId,
|
||||||
|
requestOptions,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return { mutationFn, ...mutationOptions };
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ProvisionEmployeeAgentEmployeesEmployeeIdProvisionPostMutationResult =
|
||||||
|
NonNullable<
|
||||||
|
Awaited<
|
||||||
|
ReturnType<typeof provisionEmployeeAgentEmployeesEmployeeIdProvisionPost>
|
||||||
|
>
|
||||||
|
>;
|
||||||
|
|
||||||
|
export type ProvisionEmployeeAgentEmployeesEmployeeIdProvisionPostMutationError =
|
||||||
|
HTTPValidationError;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @summary Provision Employee Agent
|
||||||
|
*/
|
||||||
|
export const useProvisionEmployeeAgentEmployeesEmployeeIdProvisionPost = <
|
||||||
|
TError = HTTPValidationError,
|
||||||
|
TContext = unknown,
|
||||||
|
>(
|
||||||
|
options?: {
|
||||||
|
mutation?: UseMutationOptions<
|
||||||
|
Awaited<
|
||||||
|
ReturnType<
|
||||||
|
typeof provisionEmployeeAgentEmployeesEmployeeIdProvisionPost
|
||||||
|
>
|
||||||
|
>,
|
||||||
|
TError,
|
||||||
|
{ employeeId: number },
|
||||||
|
TContext
|
||||||
|
>;
|
||||||
|
request?: SecondParameter<typeof customFetch>;
|
||||||
|
},
|
||||||
|
queryClient?: QueryClient,
|
||||||
|
): UseMutationResult<
|
||||||
|
Awaited<
|
||||||
|
ReturnType<typeof provisionEmployeeAgentEmployeesEmployeeIdProvisionPost>
|
||||||
|
>,
|
||||||
|
TError,
|
||||||
|
{ employeeId: number },
|
||||||
|
TContext
|
||||||
|
> => {
|
||||||
|
return useMutation(
|
||||||
|
getProvisionEmployeeAgentEmployeesEmployeeIdProvisionPostMutationOptions(
|
||||||
|
options,
|
||||||
|
),
|
||||||
|
queryClient,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* @summary Deprovision Employee Agent
|
||||||
|
*/
|
||||||
|
export type deprovisionEmployeeAgentEmployeesEmployeeIdDeprovisionPostResponse200 =
|
||||||
|
{
|
||||||
|
data: Employee;
|
||||||
|
status: 200;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type deprovisionEmployeeAgentEmployeesEmployeeIdDeprovisionPostResponse422 =
|
||||||
|
{
|
||||||
|
data: HTTPValidationError;
|
||||||
|
status: 422;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type deprovisionEmployeeAgentEmployeesEmployeeIdDeprovisionPostResponseSuccess =
|
||||||
|
deprovisionEmployeeAgentEmployeesEmployeeIdDeprovisionPostResponse200 & {
|
||||||
|
headers: Headers;
|
||||||
|
};
|
||||||
|
export type deprovisionEmployeeAgentEmployeesEmployeeIdDeprovisionPostResponseError =
|
||||||
|
deprovisionEmployeeAgentEmployeesEmployeeIdDeprovisionPostResponse422 & {
|
||||||
|
headers: Headers;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type deprovisionEmployeeAgentEmployeesEmployeeIdDeprovisionPostResponse =
|
||||||
|
| deprovisionEmployeeAgentEmployeesEmployeeIdDeprovisionPostResponseSuccess
|
||||||
|
| deprovisionEmployeeAgentEmployeesEmployeeIdDeprovisionPostResponseError;
|
||||||
|
|
||||||
|
export const getDeprovisionEmployeeAgentEmployeesEmployeeIdDeprovisionPostUrl =
|
||||||
|
(employeeId: number) => {
|
||||||
|
return `/employees/${employeeId}/deprovision`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const deprovisionEmployeeAgentEmployeesEmployeeIdDeprovisionPost =
|
||||||
|
async (
|
||||||
|
employeeId: number,
|
||||||
|
options?: RequestInit,
|
||||||
|
): Promise<deprovisionEmployeeAgentEmployeesEmployeeIdDeprovisionPostResponse> => {
|
||||||
|
return customFetch<deprovisionEmployeeAgentEmployeesEmployeeIdDeprovisionPostResponse>(
|
||||||
|
getDeprovisionEmployeeAgentEmployeesEmployeeIdDeprovisionPostUrl(
|
||||||
|
employeeId,
|
||||||
|
),
|
||||||
|
{
|
||||||
|
...options,
|
||||||
|
method: "POST",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getDeprovisionEmployeeAgentEmployeesEmployeeIdDeprovisionPostMutationOptions =
|
||||||
|
<TError = HTTPValidationError, TContext = unknown>(options?: {
|
||||||
|
mutation?: UseMutationOptions<
|
||||||
|
Awaited<
|
||||||
|
ReturnType<
|
||||||
|
typeof deprovisionEmployeeAgentEmployeesEmployeeIdDeprovisionPost
|
||||||
|
>
|
||||||
|
>,
|
||||||
|
TError,
|
||||||
|
{ employeeId: number },
|
||||||
|
TContext
|
||||||
|
>;
|
||||||
|
request?: SecondParameter<typeof customFetch>;
|
||||||
|
}): UseMutationOptions<
|
||||||
|
Awaited<
|
||||||
|
ReturnType<
|
||||||
|
typeof deprovisionEmployeeAgentEmployeesEmployeeIdDeprovisionPost
|
||||||
|
>
|
||||||
|
>,
|
||||||
|
TError,
|
||||||
|
{ employeeId: number },
|
||||||
|
TContext
|
||||||
|
> => {
|
||||||
|
const mutationKey = [
|
||||||
|
"deprovisionEmployeeAgentEmployeesEmployeeIdDeprovisionPost",
|
||||||
|
];
|
||||||
|
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<
|
||||||
|
ReturnType<
|
||||||
|
typeof deprovisionEmployeeAgentEmployeesEmployeeIdDeprovisionPost
|
||||||
|
>
|
||||||
|
>,
|
||||||
|
{ employeeId: number }
|
||||||
|
> = (props) => {
|
||||||
|
const { employeeId } = props ?? {};
|
||||||
|
|
||||||
|
return deprovisionEmployeeAgentEmployeesEmployeeIdDeprovisionPost(
|
||||||
|
employeeId,
|
||||||
|
requestOptions,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return { mutationFn, ...mutationOptions };
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DeprovisionEmployeeAgentEmployeesEmployeeIdDeprovisionPostMutationResult =
|
||||||
|
NonNullable<
|
||||||
|
Awaited<
|
||||||
|
ReturnType<
|
||||||
|
typeof deprovisionEmployeeAgentEmployeesEmployeeIdDeprovisionPost
|
||||||
|
>
|
||||||
|
>
|
||||||
|
>;
|
||||||
|
|
||||||
|
export type DeprovisionEmployeeAgentEmployeesEmployeeIdDeprovisionPostMutationError =
|
||||||
|
HTTPValidationError;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @summary Deprovision Employee Agent
|
||||||
|
*/
|
||||||
|
export const useDeprovisionEmployeeAgentEmployeesEmployeeIdDeprovisionPost = <
|
||||||
|
TError = HTTPValidationError,
|
||||||
|
TContext = unknown,
|
||||||
|
>(
|
||||||
|
options?: {
|
||||||
|
mutation?: UseMutationOptions<
|
||||||
|
Awaited<
|
||||||
|
ReturnType<
|
||||||
|
typeof deprovisionEmployeeAgentEmployeesEmployeeIdDeprovisionPost
|
||||||
|
>
|
||||||
|
>,
|
||||||
|
TError,
|
||||||
|
{ employeeId: number },
|
||||||
|
TContext
|
||||||
|
>;
|
||||||
|
request?: SecondParameter<typeof customFetch>;
|
||||||
|
},
|
||||||
|
queryClient?: QueryClient,
|
||||||
|
): UseMutationResult<
|
||||||
|
Awaited<
|
||||||
|
ReturnType<
|
||||||
|
typeof deprovisionEmployeeAgentEmployeesEmployeeIdDeprovisionPost
|
||||||
|
>
|
||||||
|
>,
|
||||||
|
TError,
|
||||||
|
{ employeeId: number },
|
||||||
|
TContext
|
||||||
|
> => {
|
||||||
|
return useMutation(
|
||||||
|
getDeprovisionEmployeeAgentEmployeesEmployeeIdDeprovisionPostMutationOptions(
|
||||||
|
options,
|
||||||
|
),
|
||||||
|
queryClient,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
@@ -204,6 +204,10 @@ export function useListProjectsProjectsGet<
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* Create a project.
|
||||||
|
|
||||||
|
Keep operation atomic: flush to get id, log activity, then commit once.
|
||||||
|
Translate DB integrity errors to 409s.
|
||||||
* @summary Create Project
|
* @summary Create Project
|
||||||
*/
|
*/
|
||||||
export type createProjectProjectsPostResponse200 = {
|
export type createProjectProjectsPostResponse200 = {
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ const NAV = [
|
|||||||
{ href: "/kanban", label: "Kanban" },
|
{ href: "/kanban", label: "Kanban" },
|
||||||
{ href: "/departments", label: "Departments" },
|
{ href: "/departments", label: "Departments" },
|
||||||
{ href: "/people", label: "People" },
|
{ href: "/people", label: "People" },
|
||||||
{ href: "/hr", label: "HR" },
|
|
||||||
];
|
];
|
||||||
|
|
||||||
export function Shell({ children }: { children: React.ReactNode }) {
|
export function Shell({ children }: { children: React.ReactNode }) {
|
||||||
|
|||||||
@@ -20,10 +20,10 @@ export default function DepartmentsPage() {
|
|||||||
const [headId, setHeadId] = useState<string>("");
|
const [headId, setHeadId] = useState<string>("");
|
||||||
|
|
||||||
const departments = useListDepartmentsDepartmentsGet();
|
const departments = useListDepartmentsDepartmentsGet();
|
||||||
const departmentList = departments.data ?? [];
|
const departmentList = departments.data?.status === 200 ? departments.data.data : [];
|
||||||
const employees = useListEmployeesEmployeesGet();
|
const employees = useListEmployeesEmployeesGet();
|
||||||
|
|
||||||
const employeeList = employees.data ?? [];
|
const employeeList = employees.data?.status === 200 ? employees.data.data : [];
|
||||||
|
|
||||||
const createDepartment = useCreateDepartmentDepartmentsPost({
|
const createDepartment = useCreateDepartmentDepartmentsPost({
|
||||||
mutation: {
|
mutation: {
|
||||||
|
|||||||
@@ -1,355 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { useState } from "react";
|
|
||||||
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
|
|
||||||
import { Select } from "@/components/ui/select";
|
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
|
||||||
|
|
||||||
import {
|
|
||||||
useCreateHeadcountRequestHrHeadcountPost,
|
|
||||||
useCreateEmploymentActionHrActionsPost,
|
|
||||||
useListHeadcountRequestsHrHeadcountGet,
|
|
||||||
useListEmploymentActionsHrActionsGet,
|
|
||||||
useListAgentOnboardingHrOnboardingGet,
|
|
||||||
useCreateAgentOnboardingHrOnboardingPost,
|
|
||||||
useUpdateAgentOnboardingHrOnboardingOnboardingIdPatch,
|
|
||||||
} from "@/api/generated/hr/hr";
|
|
||||||
import { useListDepartmentsDepartmentsGet, useListEmployeesEmployeesGet } from "@/api/generated/org/org";
|
|
||||||
|
|
||||||
export default function HRPage() {
|
|
||||||
const departments = useListDepartmentsDepartmentsGet();
|
|
||||||
const departmentList = departments.data ?? [];
|
|
||||||
const employees = useListEmployeesEmployeesGet();
|
|
||||||
const employeeList = employees.data ?? [];
|
|
||||||
|
|
||||||
const headcount = useListHeadcountRequestsHrHeadcountGet();
|
|
||||||
const actions = useListEmploymentActionsHrActionsGet();
|
|
||||||
const onboarding = useListAgentOnboardingHrOnboardingGet();
|
|
||||||
const headcountList = headcount.data ?? [];
|
|
||||||
const actionList = actions.data ?? [];
|
|
||||||
const onboardingList = onboarding.data ?? [];
|
|
||||||
|
|
||||||
const [hcDeptId, setHcDeptId] = useState<string>("");
|
|
||||||
const [hcManagerId, setHcManagerId] = useState<string>("");
|
|
||||||
const [hcRole, setHcRole] = useState("");
|
|
||||||
const [hcType, setHcType] = useState<"human" | "agent">("human");
|
|
||||||
const [hcQty, setHcQty] = useState("1");
|
|
||||||
const [hcJust, setHcJust] = useState("");
|
|
||||||
|
|
||||||
const [actEmployeeId, setActEmployeeId] = useState<string>("");
|
|
||||||
const [actIssuerId, setActIssuerId] = useState<string>("");
|
|
||||||
const [actType, setActType] = useState("praise");
|
|
||||||
const [actNotes, setActNotes] = useState("");
|
|
||||||
|
|
||||||
|
|
||||||
const [onboardAgentName, setOnboardAgentName] = useState("");
|
|
||||||
const [onboardRole, setOnboardRole] = useState("");
|
|
||||||
const [onboardPrompt, setOnboardPrompt] = useState("");
|
|
||||||
const [onboardCronMs, setOnboardCronMs] = useState("");
|
|
||||||
const [onboardTools, setOnboardTools] = useState("");
|
|
||||||
const [onboardOwnerId, setOnboardOwnerId] = useState<string>("");
|
|
||||||
const [onboardNotes, setOnboardNotes] = useState("");
|
|
||||||
const createHeadcount = useCreateHeadcountRequestHrHeadcountPost({
|
|
||||||
mutation: {
|
|
||||||
onSuccess: () => {
|
|
||||||
setHcRole("");
|
|
||||||
setHcJust("");
|
|
||||||
setHcQty("1");
|
|
||||||
headcount.refetch();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const createAction = useCreateEmploymentActionHrActionsPost({
|
|
||||||
mutation: {
|
|
||||||
onSuccess: () => {
|
|
||||||
setActNotes("");
|
|
||||||
actions.refetch();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const createOnboarding = useCreateAgentOnboardingHrOnboardingPost({
|
|
||||||
mutation: {
|
|
||||||
onSuccess: () => {
|
|
||||||
setOnboardAgentName("");
|
|
||||||
setOnboardRole("");
|
|
||||||
setOnboardPrompt("");
|
|
||||||
setOnboardCronMs("");
|
|
||||||
setOnboardTools("");
|
|
||||||
setOnboardOwnerId("");
|
|
||||||
setOnboardNotes("");
|
|
||||||
onboarding.refetch();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const updateOnboarding = useUpdateAgentOnboardingHrOnboardingOnboardingIdPatch({
|
|
||||||
mutation: {
|
|
||||||
onSuccess: () => onboarding.refetch(),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<main className="mx-auto max-w-5xl p-6">
|
|
||||||
{headcount.isLoading || actions.isLoading || onboarding.isLoading ? (
|
|
||||||
<div className="mb-4 text-sm text-muted-foreground">Loading…</div>
|
|
||||||
) : null}
|
|
||||||
{headcount.error ? <div className="mb-4 text-sm text-destructive">{(headcount.error as Error).message}</div> : null}
|
|
||||||
{actions.error ? <div className="mb-4 text-sm text-destructive">{(actions.error as Error).message}</div> : null}
|
|
||||||
{onboarding.error ? <div className="mb-4 text-sm text-destructive">{(onboarding.error as Error).message}</div> : null}
|
|
||||||
<div className="flex items-start justify-between gap-4">
|
|
||||||
<div>
|
|
||||||
<h1 className="text-2xl font-semibold tracking-tight">HR</h1>
|
|
||||||
<p className="mt-1 text-sm text-muted-foreground">Headcount requests and employment actions.</p>
|
|
||||||
</div>
|
|
||||||
<Button variant="outline" onClick={() => { headcount.refetch(); actions.refetch(); onboarding.refetch(); departments.refetch(); employees.refetch(); }} disabled={headcount.isFetching || actions.isFetching || onboarding.isFetching || departments.isFetching || employees.isFetching}>
|
|
||||||
Refresh
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-6 grid gap-4 sm:grid-cols-2">
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>Headcount request</CardTitle>
|
|
||||||
<CardDescription>Managers request; HR fulfills later.</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="space-y-3">
|
|
||||||
{departments.isLoading ? <div className="text-sm text-muted-foreground">Loading departments…</div> : null}
|
|
||||||
{departments.error ? <div className="text-sm text-destructive">{(departments.error as Error).message}</div> : null}
|
|
||||||
{employees.isLoading ? <div className="text-sm text-muted-foreground">Loading employees…</div> : null}
|
|
||||||
{employees.error ? <div className="text-sm text-destructive">{(employees.error as Error).message}</div> : null}
|
|
||||||
<Select value={hcDeptId} onChange={(e) => setHcDeptId(e.target.value)}>
|
|
||||||
<option value="">Select department</option>
|
|
||||||
{departmentList.map((d) => (
|
|
||||||
<option key={d.id ?? d.name} value={d.id ?? ""}>{d.name}</option>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
<Select value={hcManagerId} onChange={(e) => setHcManagerId(e.target.value)}>
|
|
||||||
<option value="">Requesting manager</option>
|
|
||||||
{employeeList.map((e) => (
|
|
||||||
<option key={e.id ?? e.name} value={e.id ?? ""}>{e.name}</option>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
<Input placeholder="Role title" value={hcRole} onChange={(e) => setHcRole(e.target.value)} />
|
|
||||||
<div className="grid grid-cols-2 gap-2">
|
|
||||||
<Select value={hcType} onChange={(e) => setHcType(e.target.value === "agent" ? "agent" : "human")}>
|
|
||||||
<option value="human">human</option>
|
|
||||||
<option value="agent">agent</option>
|
|
||||||
</Select>
|
|
||||||
<Input placeholder="Quantity" value={hcQty} onChange={(e) => setHcQty(e.target.value)} />
|
|
||||||
</div>
|
|
||||||
<Textarea placeholder="Justification (optional)" value={hcJust} onChange={(e) => setHcJust(e.target.value)} />
|
|
||||||
<Button
|
|
||||||
onClick={() =>
|
|
||||||
createHeadcount.mutate({
|
|
||||||
data: {
|
|
||||||
department_id: Number(hcDeptId),
|
|
||||||
requested_by_manager_id: Number(hcManagerId),
|
|
||||||
role_title: hcRole,
|
|
||||||
employee_type: hcType,
|
|
||||||
quantity: Number(hcQty || "1"),
|
|
||||||
justification: hcJust.trim() ? hcJust : null,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
disabled={!hcDeptId || !hcManagerId || !hcRole.trim() || createHeadcount.isPending}
|
|
||||||
>
|
|
||||||
Submit
|
|
||||||
</Button>
|
|
||||||
{createHeadcount.error ? (
|
|
||||||
<div className="text-sm text-destructive">{(createHeadcount.error as Error).message}</div>
|
|
||||||
) : null}
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>Employment action</CardTitle>
|
|
||||||
<CardDescription>Log HR actions (praise/warning/pip/termination).</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="space-y-3">
|
|
||||||
<Select value={actEmployeeId} onChange={(e) => setActEmployeeId(e.target.value)}>
|
|
||||||
<option value="">Employee</option>
|
|
||||||
{employeeList.map((e) => (
|
|
||||||
<option key={e.id ?? e.name} value={e.id ?? ""}>{e.name}</option>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
<Select value={actIssuerId} onChange={(e) => setActIssuerId(e.target.value)}>
|
|
||||||
<option value="">Issued by</option>
|
|
||||||
{employeeList.map((e) => (
|
|
||||||
<option key={e.id ?? e.name} value={e.id ?? ""}>{e.name}</option>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
<Select value={actType} onChange={(e) => setActType(e.target.value)}>
|
|
||||||
<option value="praise">praise</option>
|
|
||||||
<option value="warning">warning</option>
|
|
||||||
<option value="pip">pip</option>
|
|
||||||
<option value="termination">termination</option>
|
|
||||||
</Select>
|
|
||||||
<Textarea placeholder="Notes (optional)" value={actNotes} onChange={(e) => setActNotes(e.target.value)} />
|
|
||||||
<Button
|
|
||||||
onClick={() =>
|
|
||||||
createAction.mutate({
|
|
||||||
data: {
|
|
||||||
employee_id: Number(actEmployeeId),
|
|
||||||
issued_by_employee_id: Number(actIssuerId),
|
|
||||||
action_type: actType,
|
|
||||||
notes: actNotes.trim() ? actNotes : null,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
disabled={!actEmployeeId || !actIssuerId || createAction.isPending}
|
|
||||||
>
|
|
||||||
Create
|
|
||||||
</Button>
|
|
||||||
{createAction.error ? (
|
|
||||||
<div className="text-sm text-destructive">{(createAction.error as Error).message}</div>
|
|
||||||
) : null}
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card className="sm:col-span-2">
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>Recent HR activity</CardTitle>
|
|
||||||
<CardDescription>Latest headcount + actions</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="grid gap-4 sm:grid-cols-2">
|
|
||||||
<div>
|
|
||||||
<div className="mb-2 text-sm font-medium">Headcount requests</div>
|
|
||||||
<ul className="space-y-2">
|
|
||||||
{headcountList.slice(0, 10).map((r) => (
|
|
||||||
<li key={String(r.id)} className="rounded-md border p-3 text-sm">
|
|
||||||
<div className="font-medium">{r.role_title} × {r.quantity} ({r.employee_type})</div>
|
|
||||||
<div className="text-xs text-muted-foreground">dept #{r.department_id} · status: {r.status}</div>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
{headcountList.length === 0 ? (
|
|
||||||
<li className="text-sm text-muted-foreground">None yet.</li>
|
|
||||||
) : null}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div className="mb-2 text-sm font-medium">Employment actions</div>
|
|
||||||
<ul className="space-y-2">
|
|
||||||
{actionList.slice(0, 10).map((a) => (
|
|
||||||
<li key={String(a.id)} className="rounded-md border p-3 text-sm">
|
|
||||||
<div className="font-medium">{a.action_type} → employee #{a.employee_id}</div>
|
|
||||||
<div className="text-xs text-muted-foreground">issued by #{a.issued_by_employee_id}</div>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
{actionList.length === 0 ? (
|
|
||||||
<li className="text-sm text-muted-foreground">None yet.</li>
|
|
||||||
) : null}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
|
|
||||||
<div className="mt-6 grid gap-4">
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>Agent onboarding</CardTitle>
|
|
||||||
<CardDescription>HR logs prompts, cron, tools, and spawn status (Mission Control only).</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="grid gap-4 sm:grid-cols-2">
|
|
||||||
<div className="space-y-3">
|
|
||||||
<Input placeholder="Agent name" value={onboardAgentName} onChange={(e) => setOnboardAgentName(e.target.value)} />
|
|
||||||
<Input placeholder="Role/title" value={onboardRole} onChange={(e) => setOnboardRole(e.target.value)} />
|
|
||||||
<Textarea placeholder="Prompt / system instructions" value={onboardPrompt} onChange={(e) => setOnboardPrompt(e.target.value)} />
|
|
||||||
<Input placeholder="Cron interval ms (e.g. 300000)" value={onboardCronMs} onChange={(e) => setOnboardCronMs(e.target.value)} />
|
|
||||||
<Textarea placeholder="Tools/permissions (JSON or text)" value={onboardTools} onChange={(e) => setOnboardTools(e.target.value)} />
|
|
||||||
<Select value={onboardOwnerId} onChange={(e) => setOnboardOwnerId(e.target.value)}>
|
|
||||||
<option value="">Owner (HR)</option>
|
|
||||||
{employeeList.map((e) => (
|
|
||||||
<option key={e.id ?? e.name} value={e.id ?? ""}>{e.name}</option>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
<Textarea placeholder="Notes" value={onboardNotes} onChange={(e) => setOnboardNotes(e.target.value)} />
|
|
||||||
<Button
|
|
||||||
onClick={() =>
|
|
||||||
createOnboarding.mutate({
|
|
||||||
data: {
|
|
||||||
agent_name: onboardAgentName,
|
|
||||||
role_title: onboardRole,
|
|
||||||
prompt: onboardPrompt,
|
|
||||||
cron_interval_ms: onboardCronMs ? Number(onboardCronMs) : null,
|
|
||||||
tools_json: onboardTools.trim() ? onboardTools : null,
|
|
||||||
owner_hr_id: onboardOwnerId ? Number(onboardOwnerId) : null,
|
|
||||||
status: "planned",
|
|
||||||
notes: onboardNotes.trim() ? onboardNotes : null,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
disabled={!onboardAgentName.trim() || !onboardRole.trim() || !onboardPrompt.trim() || createOnboarding.isPending || employees.isFetching}
|
|
||||||
>
|
|
||||||
Create onboarding
|
|
||||||
</Button>
|
|
||||||
{createOnboarding.error ? (
|
|
||||||
<div className="text-sm text-destructive">{(createOnboarding.error as Error).message}</div>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div className="mb-2 text-sm font-medium">Current onboardings</div>
|
|
||||||
<ul className="space-y-2">
|
|
||||||
{onboardingList.map((o) => (
|
|
||||||
<li key={String(o.id)} className="rounded-md border p-3 text-sm">
|
|
||||||
<div className="font-medium">{o.agent_name} · {o.role_title}</div>
|
|
||||||
<div className="text-xs text-muted-foreground">status: {o.status} · cron: {o.cron_interval_ms ?? "—"}</div>
|
|
||||||
<div className="mt-2 grid gap-2">
|
|
||||||
<Select
|
|
||||||
value={o.status ?? ""}
|
|
||||||
onChange={(e) =>
|
|
||||||
updateOnboarding.mutate({ onboardingId: Number(o.id), data: { status: e.target.value || null } })
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<option value="planned">planned</option>
|
|
||||||
<option value="spawning">spawning</option>
|
|
||||||
<option value="spawned">spawned</option>
|
|
||||||
<option value="verified">verified</option>
|
|
||||||
<option value="blocked">blocked</option>
|
|
||||||
</Select>
|
|
||||||
<Input
|
|
||||||
placeholder="Spawned agent id"
|
|
||||||
defaultValue={o.spawned_agent_id ?? ""}
|
|
||||||
onBlur={(e) =>
|
|
||||||
updateOnboarding.mutate({ onboardingId: Number(o.id), data: { spawned_agent_id: e.currentTarget.value || null } })
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Input
|
|
||||||
placeholder="Session key"
|
|
||||||
defaultValue={o.session_key ?? ""}
|
|
||||||
onBlur={(e) =>
|
|
||||||
updateOnboarding.mutate({ onboardingId: Number(o.id), data: { session_key: e.currentTarget.value || null } })
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Textarea
|
|
||||||
placeholder="Notes"
|
|
||||||
defaultValue={o.notes ?? ""}
|
|
||||||
onBlur={(e) =>
|
|
||||||
updateOnboarding.mutate({ onboardingId: Number(o.id), data: { notes: e.currentTarget.value || null } })
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
{onboardingList.length === 0 ? (
|
|
||||||
<li className="text-sm text-muted-foreground">No onboarding records yet.</li>
|
|
||||||
) : null}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
{updateOnboarding.error ? (
|
|
||||||
<div className="mt-2 text-sm text-destructive">{(updateOnboarding.error as Error).message}</div>
|
|
||||||
) : null}
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -14,10 +14,10 @@ const STATUSES = ["backlog", "ready", "in_progress", "review", "blocked", "done"
|
|||||||
|
|
||||||
export default function KanbanPage() {
|
export default function KanbanPage() {
|
||||||
const projects = useListProjectsProjectsGet();
|
const projects = useListProjectsProjectsGet();
|
||||||
const projectList = projects.data ?? [];
|
const projectList = projects.data?.data ?? [];
|
||||||
|
|
||||||
const employees = useListEmployeesEmployeesGet();
|
const employees = useListEmployeesEmployeesGet();
|
||||||
const employeeList = useMemo(() => employees.data ?? [], [employees.data]);
|
const employeeList = useMemo(() => employees.data?.data ?? [], [employees.data]);
|
||||||
|
|
||||||
const [projectId, setProjectId] = useState<string>("");
|
const [projectId, setProjectId] = useState<string>("");
|
||||||
const [assigneeId, setAssigneeId] = useState<string>("");
|
const [assigneeId, setAssigneeId] = useState<string>("");
|
||||||
@@ -35,7 +35,7 @@ export default function KanbanPage() {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
const taskList = useMemo(() => tasks.data ?? [], [tasks.data]);
|
const taskList = useMemo(() => (tasks.data?.status === 200 ? tasks.data.data : []), [tasks.data]);
|
||||||
|
|
||||||
const updateTask = useUpdateTaskTasksTaskIdPatch({
|
const updateTask = useUpdateTaskTasksTaskIdPatch({
|
||||||
mutation: {
|
mutation: {
|
||||||
|
|||||||
@@ -17,12 +17,12 @@ import { useListActivitiesActivitiesGet } from "@/api/generated/activities/activ
|
|||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
const projects = useListProjectsProjectsGet();
|
const projects = useListProjectsProjectsGet();
|
||||||
const projectList = projects.data ?? [];
|
const projectList = projects.data?.status === 200 ? projects.data.data : [];
|
||||||
const departments = useListDepartmentsDepartmentsGet();
|
const departments = useListDepartmentsDepartmentsGet();
|
||||||
const departmentList = departments.data ?? [];
|
const departmentList = departments.data?.status === 200 ? departments.data.data : [];
|
||||||
const employees = useListEmployeesEmployeesGet();
|
const employees = useListEmployeesEmployeesGet();
|
||||||
const activities = useListActivitiesActivitiesGet({ limit: 20 });
|
const activities = useListActivitiesActivitiesGet({ limit: 20 });
|
||||||
const employeeList = employees.data ?? [];
|
const employeeList = employees.data?.status === 200 ? employees.data.data : [];
|
||||||
const activityList = normalizeActivities(activities.data);
|
const activityList = normalizeActivities(activities.data);
|
||||||
|
|
||||||
const [projectName, setProjectName] = useState("");
|
const [projectName, setProjectName] = useState("");
|
||||||
|
|||||||
@@ -24,8 +24,8 @@ export default function PeoplePage() {
|
|||||||
|
|
||||||
const employees = useListEmployeesEmployeesGet();
|
const employees = useListEmployeesEmployeesGet();
|
||||||
const departments = useListDepartmentsDepartmentsGet();
|
const departments = useListDepartmentsDepartmentsGet();
|
||||||
const departmentList = useMemo(() => departments.data ?? [], [departments.data]);
|
const departmentList = useMemo(() => (departments.data?.status === 200 ? departments.data.data : []), [departments.data]);
|
||||||
const employeeList = useMemo(() => employees.data ?? [], [employees.data]);
|
const employeeList = useMemo(() => (employees.data?.status === 200 ? employees.data.data : []), [employees.data]);
|
||||||
|
|
||||||
const createEmployee = useCreateEmployeeEmployeesPost({
|
const createEmployee = useCreateEmployeeEmployeesPost({
|
||||||
mutation: {
|
mutation: {
|
||||||
|
|||||||
@@ -46,14 +46,14 @@ export default function ProjectDetailPage() {
|
|||||||
const projectId = Number(params?.id);
|
const projectId = Number(params?.id);
|
||||||
|
|
||||||
const projects = useListProjectsProjectsGet();
|
const projects = useListProjectsProjectsGet();
|
||||||
const projectList = projects.data ?? [];
|
const projectList = projects.data?.status === 200 ? projects.data.data : [];
|
||||||
const project = projectList.find((p) => p.id === projectId);
|
const project = projectList.find((p) => p.id === projectId);
|
||||||
|
|
||||||
const employees = useListEmployeesEmployeesGet();
|
const employees = useListEmployeesEmployeesGet();
|
||||||
const employeeList = employees.data ?? [];
|
const employeeList = employees.data?.status === 200 ? employees.data.data : [];
|
||||||
|
|
||||||
const members = useListProjectMembersProjectsProjectIdMembersGet(projectId);
|
const members = useListProjectMembersProjectsProjectIdMembersGet(projectId);
|
||||||
const memberList = members.data ?? [];
|
const memberList = members.data?.status === 200 ? members.data.data : [];
|
||||||
const addMember = useAddProjectMemberProjectsProjectIdMembersPost({
|
const addMember = useAddProjectMemberProjectsProjectIdMembersPost({
|
||||||
mutation: { onSuccess: () => members.refetch() },
|
mutation: { onSuccess: () => members.refetch() },
|
||||||
});
|
});
|
||||||
@@ -65,7 +65,7 @@ export default function ProjectDetailPage() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const tasks = useListTasksTasksGet({ project_id: projectId });
|
const tasks = useListTasksTasksGet({ project_id: projectId });
|
||||||
const taskList = tasks.data ?? [];
|
const taskList = tasks.data?.status === 200 ? tasks.data.data : [];
|
||||||
const createTask = useCreateTaskTasksPost({
|
const createTask = useCreateTaskTasksPost({
|
||||||
mutation: { onSuccess: () => tasks.refetch() },
|
mutation: { onSuccess: () => tasks.refetch() },
|
||||||
});
|
});
|
||||||
@@ -89,7 +89,7 @@ export default function ProjectDetailPage() {
|
|||||||
{ task_id: commentTaskId ?? 0 },
|
{ task_id: commentTaskId ?? 0 },
|
||||||
{ query: { enabled: Boolean(commentTaskId) } },
|
{ query: { enabled: Boolean(commentTaskId) } },
|
||||||
);
|
);
|
||||||
const commentList = comments.data ?? [];
|
const commentList = comments.data?.status === 200 ? comments.data.data : [];
|
||||||
const addComment = useCreateTaskCommentTaskCommentsPost({
|
const addComment = useCreateTaskCommentTaskCommentsPost({
|
||||||
mutation: {
|
mutation: {
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ export default function ProjectsPage() {
|
|||||||
const [name, setName] = useState("");
|
const [name, setName] = useState("");
|
||||||
|
|
||||||
const projects = useListProjectsProjectsGet();
|
const projects = useListProjectsProjectsGet();
|
||||||
const projectList = projects.data ?? [];
|
const projectList = projects.data?.status === 200 ? projects.data.data : [];
|
||||||
const createProject = useCreateProjectProjectsPost({
|
const createProject = useCreateProjectProjectsPost({
|
||||||
mutation: {
|
mutation: {
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user