People: better default agent prompt; remove HR UI; regen API types; fix list response handling
This commit is contained in:
@@ -24,7 +24,17 @@ def _default_agent_prompt(emp: Employee) -> str:
|
||||
|
||||
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"
|
||||
|
||||
@@ -13,4 +13,6 @@ export interface Employee {
|
||||
manager_id?: number | null;
|
||||
title?: string | null;
|
||||
status?: string;
|
||||
openclaw_session_key?: string | null;
|
||||
notify_enabled?: boolean;
|
||||
}
|
||||
|
||||
@@ -12,4 +12,6 @@ export interface EmployeeCreate {
|
||||
manager_id?: number | null;
|
||||
title?: string | null;
|
||||
status?: string;
|
||||
openclaw_session_key?: string | null;
|
||||
notify_enabled?: boolean;
|
||||
}
|
||||
|
||||
@@ -12,4 +12,6 @@ export interface EmployeeUpdate {
|
||||
manager_id?: number | null;
|
||||
title?: string | null;
|
||||
status?: string | null;
|
||||
openclaw_session_key?: string | null;
|
||||
notify_enabled?: boolean | null;
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ export interface TaskComment {
|
||||
id?: number | null;
|
||||
task_id: number;
|
||||
author_employee_id?: number | null;
|
||||
reply_to_comment_id?: number | null;
|
||||
body: string;
|
||||
created_at?: string;
|
||||
}
|
||||
|
||||
@@ -8,5 +8,6 @@
|
||||
export interface TaskCommentCreate {
|
||||
task_id: number;
|
||||
author_employee_id?: number | null;
|
||||
reply_to_comment_id?: number | null;
|
||||
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
|
||||
*/
|
||||
export type createDepartmentDepartmentsPostResponse200 = {
|
||||
@@ -863,3 +867,299 @@ export const useUpdateEmployeeEmployeesEmployeeIdPatch = <
|
||||
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
|
||||
*/
|
||||
export type createProjectProjectsPostResponse200 = {
|
||||
|
||||
@@ -11,7 +11,6 @@ const NAV = [
|
||||
{ href: "/kanban", label: "Kanban" },
|
||||
{ href: "/departments", label: "Departments" },
|
||||
{ href: "/people", label: "People" },
|
||||
{ href: "/hr", label: "HR" },
|
||||
];
|
||||
|
||||
export function Shell({ children }: { children: React.ReactNode }) {
|
||||
|
||||
@@ -20,10 +20,10 @@ export default function DepartmentsPage() {
|
||||
const [headId, setHeadId] = useState<string>("");
|
||||
|
||||
const departments = useListDepartmentsDepartmentsGet();
|
||||
const departmentList = departments.data ?? [];
|
||||
const departmentList = departments.data?.status === 200 ? departments.data.data : [];
|
||||
const employees = useListEmployeesEmployeesGet();
|
||||
|
||||
const employeeList = employees.data ?? [];
|
||||
const employeeList = employees.data?.status === 200 ? employees.data.data : [];
|
||||
|
||||
const createDepartment = useCreateDepartmentDepartmentsPost({
|
||||
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() {
|
||||
const projects = useListProjectsProjectsGet();
|
||||
const projectList = projects.data ?? [];
|
||||
const projectList = projects.data?.data ?? [];
|
||||
|
||||
const employees = useListEmployeesEmployeesGet();
|
||||
const employeeList = useMemo(() => employees.data ?? [], [employees.data]);
|
||||
const employeeList = useMemo(() => employees.data?.data ?? [], [employees.data]);
|
||||
|
||||
const [projectId, setProjectId] = 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({
|
||||
mutation: {
|
||||
|
||||
@@ -17,12 +17,12 @@ import { useListActivitiesActivitiesGet } from "@/api/generated/activities/activ
|
||||
|
||||
export default function Home() {
|
||||
const projects = useListProjectsProjectsGet();
|
||||
const projectList = projects.data ?? [];
|
||||
const projectList = projects.data?.status === 200 ? projects.data.data : [];
|
||||
const departments = useListDepartmentsDepartmentsGet();
|
||||
const departmentList = departments.data ?? [];
|
||||
const departmentList = departments.data?.status === 200 ? departments.data.data : [];
|
||||
const employees = useListEmployeesEmployeesGet();
|
||||
const activities = useListActivitiesActivitiesGet({ limit: 20 });
|
||||
const employeeList = employees.data ?? [];
|
||||
const employeeList = employees.data?.status === 200 ? employees.data.data : [];
|
||||
const activityList = normalizeActivities(activities.data);
|
||||
|
||||
const [projectName, setProjectName] = useState("");
|
||||
|
||||
@@ -24,8 +24,8 @@ export default function PeoplePage() {
|
||||
|
||||
const employees = useListEmployeesEmployeesGet();
|
||||
const departments = useListDepartmentsDepartmentsGet();
|
||||
const departmentList = useMemo(() => departments.data ?? [], [departments.data]);
|
||||
const employeeList = useMemo(() => employees.data ?? [], [employees.data]);
|
||||
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 createEmployee = useCreateEmployeeEmployeesPost({
|
||||
mutation: {
|
||||
|
||||
@@ -46,14 +46,14 @@ export default function ProjectDetailPage() {
|
||||
const projectId = Number(params?.id);
|
||||
|
||||
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 employees = useListEmployeesEmployeesGet();
|
||||
const employeeList = employees.data ?? [];
|
||||
const employeeList = employees.data?.status === 200 ? employees.data.data : [];
|
||||
|
||||
const members = useListProjectMembersProjectsProjectIdMembersGet(projectId);
|
||||
const memberList = members.data ?? [];
|
||||
const memberList = members.data?.status === 200 ? members.data.data : [];
|
||||
const addMember = useAddProjectMemberProjectsProjectIdMembersPost({
|
||||
mutation: { onSuccess: () => members.refetch() },
|
||||
});
|
||||
@@ -65,7 +65,7 @@ export default function ProjectDetailPage() {
|
||||
});
|
||||
|
||||
const tasks = useListTasksTasksGet({ project_id: projectId });
|
||||
const taskList = tasks.data ?? [];
|
||||
const taskList = tasks.data?.status === 200 ? tasks.data.data : [];
|
||||
const createTask = useCreateTaskTasksPost({
|
||||
mutation: { onSuccess: () => tasks.refetch() },
|
||||
});
|
||||
@@ -89,7 +89,7 @@ export default function ProjectDetailPage() {
|
||||
{ task_id: commentTaskId ?? 0 },
|
||||
{ query: { enabled: Boolean(commentTaskId) } },
|
||||
);
|
||||
const commentList = comments.data ?? [];
|
||||
const commentList = comments.data?.status === 200 ? comments.data.data : [];
|
||||
const addComment = useCreateTaskCommentTaskCommentsPost({
|
||||
mutation: {
|
||||
onSuccess: () => {
|
||||
|
||||
@@ -17,7 +17,7 @@ export default function ProjectsPage() {
|
||||
const [name, setName] = useState("");
|
||||
|
||||
const projects = useListProjectsProjectsGet();
|
||||
const projectList = projects.data ?? [];
|
||||
const projectList = projects.data?.status === 200 ? projects.data.data : [];
|
||||
const createProject = useCreateProjectProjectsPost({
|
||||
mutation: {
|
||||
onSuccess: () => {
|
||||
|
||||
Reference in New Issue
Block a user