182 lines
5.3 KiB
TypeScript
182 lines
5.3 KiB
TypeScript
/** 岗位角色配置 API:岗位会商工作视角,不等同于组织岗位/RBAC。 */
|
|
|
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
|
|
import { apiFetch } from "@/core/api/fetch-client";
|
|
import { getBackendBaseURL } from "@/core/config";
|
|
import { OptType, writeYLog } from "@/core/log/y-log";
|
|
|
|
export type PositionRoleType = "standard" | "intent";
|
|
|
|
export interface PositionRole {
|
|
id: string;
|
|
name: string;
|
|
description: string;
|
|
roleType: PositionRoleType;
|
|
enabled: boolean;
|
|
isBuiltin: boolean;
|
|
sortOrder: number;
|
|
updatedBy?: string | null;
|
|
updatedAt?: string | null;
|
|
/** True only when the API is unavailable and the UI is showing built-ins. */
|
|
isFallback?: boolean;
|
|
}
|
|
|
|
interface ServerPositionRole {
|
|
id: string;
|
|
name: string;
|
|
description: string;
|
|
role_type: PositionRoleType;
|
|
enabled: boolean;
|
|
is_builtin: boolean;
|
|
sort_order: number;
|
|
updated_by?: string | null;
|
|
updated_at?: string | null;
|
|
}
|
|
|
|
const BASE = () => `${getBackendBaseURL()}/api/position-roles`;
|
|
export const POSITION_ROLES_QUERY_KEY = ["position-roles"] as const;
|
|
|
|
/**
|
|
* Safe first-run directory for the position-roundtable workbench.
|
|
*
|
|
* The backend persists exactly the same values in ``position_roles``. Keeping
|
|
* this client copy means an old/offline gateway cannot turn the administrator
|
|
* page or the roundtable selector into an empty screen during deployment.
|
|
*/
|
|
export const DEFAULT_POSITION_ROLES: readonly PositionRole[] = [
|
|
{
|
|
id: "intelligence",
|
|
name: "测试分析岗",
|
|
description: "识别任务意图、补齐约束并确认业务链条",
|
|
roleType: "intent",
|
|
enabled: true,
|
|
isBuiltin: true,
|
|
sortOrder: 0,
|
|
},
|
|
{
|
|
id: "planning",
|
|
name: "组织测试岗",
|
|
description: "承接上游信息并进行组织测试",
|
|
roleType: "standard",
|
|
enabled: true,
|
|
isBuiltin: true,
|
|
sortOrder: 1,
|
|
},
|
|
{
|
|
id: "solution",
|
|
name: "方案设计岗",
|
|
description: "承接上游结果并形成方案产物",
|
|
roleType: "standard",
|
|
enabled: true,
|
|
isBuiltin: true,
|
|
sortOrder: 2,
|
|
},
|
|
{
|
|
id: "evaluation",
|
|
name: "综合测试岗",
|
|
description: "测试链条产物并提出综合建议",
|
|
roleType: "standard",
|
|
enabled: true,
|
|
isBuiltin: true,
|
|
sortOrder: 3,
|
|
},
|
|
];
|
|
|
|
function defaultRoles(): PositionRole[] {
|
|
return DEFAULT_POSITION_ROLES.map((role) => ({ ...role, isFallback: true }));
|
|
}
|
|
|
|
function fromServer(row: ServerPositionRole): PositionRole {
|
|
return {
|
|
id: row.id,
|
|
name: row.name ?? "",
|
|
description: row.description ?? "",
|
|
roleType: row.role_type ?? "standard",
|
|
enabled: row.enabled ?? true,
|
|
isBuiltin: row.is_builtin ?? false,
|
|
sortOrder: row.sort_order ?? 0,
|
|
updatedBy: row.updated_by ?? null,
|
|
updatedAt: row.updated_at ?? null,
|
|
};
|
|
}
|
|
|
|
function toServer(row: PositionRole): ServerPositionRole {
|
|
return {
|
|
id: row.id,
|
|
name: row.name,
|
|
description: row.description,
|
|
role_type: row.roleType,
|
|
enabled: row.enabled,
|
|
is_builtin: row.isBuiltin,
|
|
sort_order: row.sortOrder,
|
|
};
|
|
}
|
|
|
|
async function readJson<T>(response: Response, context: string): Promise<T> {
|
|
if (!response.ok) {
|
|
const payload = await response.json().catch(() => null as unknown);
|
|
const detail =
|
|
payload && typeof payload === "object" && "detail" in payload
|
|
? String((payload as { detail: unknown }).detail)
|
|
: `${context} failed (${response.status})`;
|
|
throw new Error(detail);
|
|
}
|
|
return (await response.json()) as T;
|
|
}
|
|
|
|
export async function listPositionRoles(): Promise<PositionRole[]> {
|
|
try {
|
|
const response = await apiFetch(BASE(), { method: "GET" });
|
|
const payload = await readJson<{ roles: ServerPositionRole[] }>(response, "list position roles");
|
|
const roles = (payload.roles ?? []).map(fromServer);
|
|
// An empty directory is never valid: it means an old gateway is running
|
|
// or the first-run seed has not completed yet. Keep the page usable.
|
|
return roles.length > 0 ? roles : defaultRoles();
|
|
} catch {
|
|
// Do not leave administrators with an empty configuration screen when an
|
|
// older deployment has not received the new router/migration yet.
|
|
return defaultRoles();
|
|
}
|
|
}
|
|
|
|
export async function replacePositionRoles(roles: PositionRole[]): Promise<PositionRole[]> {
|
|
const response = await apiFetch(BASE(), {
|
|
method: "PUT",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ roles: roles.map(toServer) }),
|
|
});
|
|
const payload = await readJson<{ roles: ServerPositionRole[] }>(response, "save position roles");
|
|
const saved = (payload.roles ?? []).map(fromServer);
|
|
writeYLog({
|
|
optType: OptType.edit,
|
|
optDetail: "CM助手-岗位角色配置-保存",
|
|
url: "/api/position-roles",
|
|
reqParam: { roles },
|
|
returnParam: saved,
|
|
});
|
|
return saved;
|
|
}
|
|
|
|
export function usePositionRoles(options?: { enabled?: boolean }) {
|
|
return useQuery({
|
|
queryKey: POSITION_ROLES_QUERY_KEY,
|
|
queryFn: listPositionRoles,
|
|
staleTime: 30_000,
|
|
refetchOnWindowFocus: false,
|
|
enabled: options?.enabled ?? true,
|
|
});
|
|
}
|
|
|
|
export function usePositionRoleActions() {
|
|
const queryClient = useQueryClient();
|
|
return {
|
|
save: useMutation({
|
|
mutationFn: replacePositionRoles,
|
|
onSuccess: (roles) => {
|
|
queryClient.setQueryData(POSITION_ROLES_QUERY_KEY, roles);
|
|
},
|
|
}),
|
|
};
|
|
}
|