forked from ChaiHao/BPOS-MVP
三面拆分⑩:工作组管理落员工区 —— ADR-011 欠账清账
建组/改组:逐岗位选人,留空即回落四策略。绑定先于策略,起单时快照进工作流; 实测改绑定后新起单的 work_group 快照即含新成员表,在途不受影响。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KeDBx2PHLavbURXB8LJJ7m
This commit is contained in:
@@ -1493,6 +1493,44 @@ export declare const upsertTriggerRoute: {
|
|||||||
}, z.core.$strip>;
|
}, z.core.$strip>;
|
||||||
}, z.core.$strip>;
|
}, z.core.$strip>;
|
||||||
};
|
};
|
||||||
|
/**
|
||||||
|
* 工作组 = 一组「岗位 → 员工」的绑定,**起单时装载、随工作流走**。
|
||||||
|
*
|
||||||
|
* 它是**绑定先于策略**,不是第五种负责人策略:起单人有组,整条流的岗位就按组指人;
|
||||||
|
* 没有组才回落四策略。装进工作流的是**快照** —— 改组不改已起的单。
|
||||||
|
*/
|
||||||
|
export declare const listWorkGroupsRoute: {
|
||||||
|
readonly method: "GET";
|
||||||
|
readonly path: "/api/gov/work-groups";
|
||||||
|
/** `ownerName` 是组长姓名的呈现补充,读表时一并带出,免得前端再对一次 id */
|
||||||
|
readonly output: z.ZodObject<{
|
||||||
|
groups: z.ZodArray<z.ZodObject<{
|
||||||
|
id: z.ZodString;
|
||||||
|
name: z.ZodString;
|
||||||
|
ownerId: z.core.$ZodBranded<z.ZodString, "StaffId", "out">;
|
||||||
|
members: z.ZodRecord<z.ZodString, z.core.$ZodBranded<z.ZodString, "StaffId", "out">>;
|
||||||
|
ownerName: z.ZodString;
|
||||||
|
}, z.core.$strip>>;
|
||||||
|
}, z.core.$strip>;
|
||||||
|
};
|
||||||
|
/** 一人一组:按 `ownerId` 落,已有即更新、没有即新建 */
|
||||||
|
export declare const saveWorkGroupRoute: {
|
||||||
|
readonly method: "PUT";
|
||||||
|
readonly path: "/api/gov/work-groups";
|
||||||
|
readonly body: z.ZodObject<{
|
||||||
|
ownerId: z.core.$ZodBranded<z.ZodString, "StaffId", "out">;
|
||||||
|
name: z.ZodString;
|
||||||
|
members: z.ZodRecord<z.ZodString, z.core.$ZodBranded<z.ZodString, "StaffId", "out">>;
|
||||||
|
}, z.core.$strip>;
|
||||||
|
readonly output: z.ZodObject<{
|
||||||
|
group: z.ZodObject<{
|
||||||
|
id: z.ZodString;
|
||||||
|
name: z.ZodString;
|
||||||
|
ownerId: z.core.$ZodBranded<z.ZodString, "StaffId", "out">;
|
||||||
|
members: z.ZodRecord<z.ZodString, z.core.$ZodBranded<z.ZodString, "StaffId", "out">>;
|
||||||
|
}, z.core.$strip>;
|
||||||
|
}, z.core.$strip>;
|
||||||
|
};
|
||||||
export declare const listActivitiesRoute: {
|
export declare const listActivitiesRoute: {
|
||||||
readonly method: "GET";
|
readonly method: "GET";
|
||||||
readonly path: "/api/gov/activities";
|
readonly path: "/api/gov/activities";
|
||||||
@@ -1905,6 +1943,7 @@ export declare const govMetricsRoute: {
|
|||||||
}, z.core.$strip>;
|
}, z.core.$strip>;
|
||||||
};
|
};
|
||||||
export type GovTasksOutput = z.infer<typeof listGovTasksRoute.output>;
|
export type GovTasksOutput = z.infer<typeof listGovTasksRoute.output>;
|
||||||
|
export type GovWorkGroupsOutput = z.infer<typeof listWorkGroupsRoute.output>;
|
||||||
export type GovApprovalsOutput = z.infer<typeof listApprovalsRoute.output>;
|
export type GovApprovalsOutput = z.infer<typeof listApprovalsRoute.output>;
|
||||||
export type GovStaffOutput = z.infer<typeof listStaffRoute.output>;
|
export type GovStaffOutput = z.infer<typeof listStaffRoute.output>;
|
||||||
export type GovGrantsOutput = z.infer<typeof listGrantsRoute.output>;
|
export type GovGrantsOutput = z.infer<typeof listGrantsRoute.output>;
|
||||||
|
|||||||
@@ -11,8 +11,10 @@ import {
|
|||||||
listGrantsRoute,
|
listGrantsRoute,
|
||||||
listLlmInstancesRoute,
|
listLlmInstancesRoute,
|
||||||
listStaffRoute,
|
listStaffRoute,
|
||||||
|
listWorkGroupsRoute,
|
||||||
positionIdSchema,
|
positionIdSchema,
|
||||||
probeLlmInstanceRoute,
|
probeLlmInstanceRoute,
|
||||||
|
saveWorkGroupRoute,
|
||||||
setGrantsRoute,
|
setGrantsRoute,
|
||||||
staffIdSchema,
|
staffIdSchema,
|
||||||
upsertLlmInstanceRoute,
|
upsertLlmInstanceRoute,
|
||||||
@@ -97,6 +99,48 @@ export function registerGovernanceRoutes(app: Hono<AppEnv>, container: Container
|
|||||||
return c.json({ grants: await container.org.setGrants(positionId, parsed.data.grants) });
|
return c.json({ grants: await container.org.setGrants(positionId, parsed.data.grants) });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/* ── 员工管理 · 工作组(ADR-011) ──────────────────────────────────── */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 全部工作组 —— 一人至多一组,按员工遍历取。
|
||||||
|
* `ownerName` 一并带出:前端要显示的是「柴浩的组」,不是一串 id。
|
||||||
|
*/
|
||||||
|
app.get(listWorkGroupsRoute.path, async (c) => {
|
||||||
|
const denied = await guardStaff(c.get('account'));
|
||||||
|
if (denied) return c.json(denied, denied.code === 'forbidden' ? 403 : 401);
|
||||||
|
const { staff } = await container.org.listStaff();
|
||||||
|
const found = await Promise.all(
|
||||||
|
staff.map(async (s) => {
|
||||||
|
const group = await container.org.workGroupOf(s.id);
|
||||||
|
return group === undefined ? undefined : { ...group, ownerName: s.name };
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
return c.json({ groups: found.filter((g) => g !== undefined) });
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 建组 / 改组 —— 按 `ownerId` 落:已有即更新(复用 id),没有即新建。
|
||||||
|
* 与 `workGroupOf` 的取组口径一致(一人一组),不给同一个人建出两个组来。
|
||||||
|
* 改组**不影响在途**:装进工作流的是起单时的快照。
|
||||||
|
*/
|
||||||
|
app.put(saveWorkGroupRoute.path, async (c) => {
|
||||||
|
const denied = await guardStaff(c.get('account'));
|
||||||
|
if (denied) return c.json(denied, denied.code === 'forbidden' ? 403 : 401);
|
||||||
|
const parsed = saveWorkGroupRoute.body.safeParse(await c.req.json());
|
||||||
|
if (!parsed.success) {
|
||||||
|
const body: ApiError = { code: 'validation-failed', message: '工作组不合 schema', detail: parsed.error.issues };
|
||||||
|
return c.json(body, 400);
|
||||||
|
}
|
||||||
|
const existing = await container.org.workGroupOf(parsed.data.ownerId);
|
||||||
|
const group = await container.org.saveWorkGroup({
|
||||||
|
...(existing === undefined ? {} : { id: existing.id }),
|
||||||
|
name: parsed.data.name,
|
||||||
|
ownerId: parsed.data.ownerId,
|
||||||
|
members: parsed.data.members,
|
||||||
|
});
|
||||||
|
return c.json({ group });
|
||||||
|
});
|
||||||
|
|
||||||
/* ── 系统管理 · LLM 实例 ──────────────────────────────────────────── */
|
/* ── 系统管理 · LLM 实例 ──────────────────────────────────────────── */
|
||||||
|
|
||||||
app.get(listLlmInstancesRoute.path, async (c) => {
|
app.get(listLlmInstancesRoute.path, async (c) => {
|
||||||
|
|||||||
@@ -45,3 +45,7 @@
|
|||||||
影响半径: contract 增 `login` / `logout` / `resolveSession` / `setPassword` 与 `IssuedSession`;
|
影响半径: contract 增 `login` / `logout` / `resolveSession` / `setPassword` 与 `IssuedSession`;
|
||||||
port 增 `SessionRepository` / `PasswordHasher`,`StaffRepository` 增两个口令读写方法;
|
port 增 `SessionRepository` / `PasswordHasher`,`StaffRepository` 增两个口令读写方法;
|
||||||
下游只有 app(装配 + 鉴权中间件),depcruise 已反查确认
|
下游只有 app(装配 + 鉴权中间件),depcruise 已反查确认
|
||||||
|
|
||||||
|
近变(2026-07-27):模块面**未变**。app 层新增工作组读写路由
|
||||||
|
(`GET|PUT /api/gov/work-groups`),组装既有的 `workGroupOf` / `saveWorkGroup`
|
||||||
|
—— ADR-011 落地时欠的那个治理界面就此清账,一人一组的口径由 app 层按 ownerId 保证。
|
||||||
|
|||||||
@@ -5,21 +5,37 @@
|
|||||||
* DOM 结构、类名(`g-*` + 色调 `t-*`)与文案原样保留,mock 常量换成服务端投影(README 平移规则 1/2/4)。
|
* DOM 结构、类名(`g-*` + 色调 `t-*`)与文案原样保留,mock 常量换成服务端投影(README 平移规则 1/2/4)。
|
||||||
* 区首页只负责导航,治理动作一律落在下钻详情页的**就地确认带**(影响预览三必答)。
|
* 区首页只负责导航,治理动作一律落在下钻详情页的**就地确认带**(影响预览三必答)。
|
||||||
*/
|
*/
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
|
import { GOVERNANCE_FUNCTION_LABEL, GRANT_ACTION_LABEL, saveWorkGroupRoute, staffIdSchema } from '@contracts';
|
||||||
|
import type { StaffId } from '@contracts';
|
||||||
|
import { call } from '@web-shared/typed-fetch.ts';
|
||||||
import {
|
import {
|
||||||
GOVERNANCE_FUNCTION_LABEL,
|
Acts,
|
||||||
GRANT_ACTION_LABEL,
|
Card,
|
||||||
} from '@contracts';
|
Chip,
|
||||||
import { Card, Chip, Clear, Entry, NList, NRow, Note, PageHead, QueryGate, St } from '@web-shared/gov/primitives.tsx';
|
Clear,
|
||||||
|
Entry,
|
||||||
|
Field,
|
||||||
|
NList,
|
||||||
|
NRow,
|
||||||
|
Note,
|
||||||
|
PageHead,
|
||||||
|
QueryGate,
|
||||||
|
Rows,
|
||||||
|
St,
|
||||||
|
} from '@web-shared/gov/primitives.tsx';
|
||||||
import type { Nav } from '@web-shared/gov/gov-route.ts';
|
import type { Nav } from '@web-shared/gov/gov-route.ts';
|
||||||
import {
|
import { useGovGrants, useGovStaff, useGovTasks, useGovWorkGroups } from '@web-shared/gov/gov-api.ts';
|
||||||
useGovGrants,
|
|
||||||
useGovStaff,
|
|
||||||
useGovTasks,
|
|
||||||
} from '@web-shared/gov/gov-api.ts';
|
|
||||||
|
|
||||||
export type HomeProps = { tab: string; nav: Nav };
|
export type HomeProps = { tab: string; nav: Nav };
|
||||||
|
|
||||||
export function PeopleHome({ tab, nav }: HomeProps) {
|
export function PeopleHome({ tab, nav }: HomeProps) {
|
||||||
|
if (tab === 'groups') return <WorkGroupsTab />;
|
||||||
|
return <StaffHome tab={tab} nav={nav} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function StaffHome({ tab, nav }: HomeProps) {
|
||||||
const q = useGovStaff();
|
const q = useGovStaff();
|
||||||
const tasks = useGovTasks();
|
const tasks = useGovTasks();
|
||||||
const staff = q.data?.staff ?? [];
|
const staff = q.data?.staff ?? [];
|
||||||
@@ -153,3 +169,201 @@ export function RolePage({ id }: { id: string }) {
|
|||||||
|
|
||||||
/* ---------------- 流程包 ---------------- */
|
/* ---------------- 流程包 ---------------- */
|
||||||
|
|
||||||
|
|
||||||
|
/* ---------------- 工作组(ADR-011) ---------------- */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 工作组 = 一组「岗位 → 员工」的绑定,**起单时装载、随工作流走**。
|
||||||
|
*
|
||||||
|
* 它是**绑定先于策略**,不是第五种负责人策略:起单人有组,整条流的岗位就按组指人;
|
||||||
|
* 没有组才回落四策略(独任 / 轮询 / 抢单 / 指派)。
|
||||||
|
* 装进工作流的是**快照** —— 改组不改已起的单,留痕不被改写。
|
||||||
|
*/
|
||||||
|
function WorkGroupsTab() {
|
||||||
|
const groups = useGovWorkGroups();
|
||||||
|
const staffQ = useGovStaff();
|
||||||
|
const [editing, setEditing] = useState<string | undefined>(undefined);
|
||||||
|
|
||||||
|
const staff = staffQ.data?.staff ?? [];
|
||||||
|
const positions = staffQ.data?.positions ?? [];
|
||||||
|
const nameOf = (id: string) => staff.find((s) => s.id === id)?.name ?? id;
|
||||||
|
const rows = groups.data?.groups ?? [];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<QueryGate query={groups}>
|
||||||
|
<>
|
||||||
|
<Note>
|
||||||
|
工作组把<b>岗位绑到人</b>:谁起的单,整条流的岗位就按谁的组指人 —— 绑定先于策略,
|
||||||
|
没绑定的岗位仍走四策略。绑定在起单那一刻<b>快照</b>进工作流,此后改组不影响已起的单。
|
||||||
|
</Note>
|
||||||
|
{rows.length === 0 ? (
|
||||||
|
<Clear>还没有工作组 —— 给谁建一个,他起的单整条流就能一次指到人。</Clear>
|
||||||
|
) : (
|
||||||
|
rows.map((g) =>
|
||||||
|
editing === g.ownerId ? (
|
||||||
|
<WorkGroupForm
|
||||||
|
key={g.id}
|
||||||
|
ownerId={g.ownerId}
|
||||||
|
ownerName={g.ownerName}
|
||||||
|
name={g.name}
|
||||||
|
members={g.members}
|
||||||
|
positions={positions}
|
||||||
|
staff={staff}
|
||||||
|
onDone={() => setEditing(undefined)}
|
||||||
|
onCancel={() => setEditing(undefined)}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Card
|
||||||
|
key={g.id}
|
||||||
|
ico="users"
|
||||||
|
title={g.name}
|
||||||
|
extra={
|
||||||
|
<button className="g-btn" onClick={() => setEditing(g.ownerId)}>
|
||||||
|
改绑定
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Rows
|
||||||
|
items={[
|
||||||
|
{ k: '组长', v: g.ownerName },
|
||||||
|
...positions.map((p) => ({
|
||||||
|
k: `${p.name} · ${p.code}`,
|
||||||
|
v: g.members[p.code] === undefined ? '未绑定 · 走四策略' : nameOf(g.members[p.code]!),
|
||||||
|
})),
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
<Card ico="user-round" title="给还没有组的人建一个">
|
||||||
|
<NList>
|
||||||
|
{staff
|
||||||
|
.filter((s) => !rows.some((g) => g.ownerId === s.id))
|
||||||
|
.map((s) =>
|
||||||
|
editing === s.id ? null : (
|
||||||
|
<NRow
|
||||||
|
key={s.id}
|
||||||
|
tone="mute"
|
||||||
|
ico="user-round"
|
||||||
|
title={s.name}
|
||||||
|
sub={`${s.position.name} · ${s.position.code}`}
|
||||||
|
right={<span className="g-mono">建组 →</span>}
|
||||||
|
onClick={() => setEditing(s.id)}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
{staff.every((s) => rows.some((g) => g.ownerId === s.id)) && <Clear>每个人都有组了。</Clear>}
|
||||||
|
</NList>
|
||||||
|
</Card>
|
||||||
|
{staff
|
||||||
|
.filter((s) => !rows.some((g) => g.ownerId === s.id) && editing === s.id)
|
||||||
|
.map((s) => (
|
||||||
|
<WorkGroupForm
|
||||||
|
key={s.id}
|
||||||
|
ownerId={s.id}
|
||||||
|
ownerName={s.name}
|
||||||
|
name={`${s.name}的工作组`}
|
||||||
|
members={{}}
|
||||||
|
positions={positions}
|
||||||
|
staff={staff}
|
||||||
|
onDone={() => setEditing(undefined)}
|
||||||
|
onCancel={() => setEditing(undefined)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
</QueryGate>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 建组 / 改组表单:逐岗位选人,留空即该岗位不绑定 */
|
||||||
|
function WorkGroupForm({
|
||||||
|
ownerId,
|
||||||
|
ownerName,
|
||||||
|
name,
|
||||||
|
members,
|
||||||
|
positions,
|
||||||
|
staff,
|
||||||
|
onDone,
|
||||||
|
onCancel,
|
||||||
|
}: {
|
||||||
|
ownerId: string;
|
||||||
|
ownerName: string;
|
||||||
|
name: string;
|
||||||
|
members: Readonly<Record<string, string>>;
|
||||||
|
positions: ReadonlyArray<{ id: string; code: string; name: string }>;
|
||||||
|
staff: ReadonlyArray<{ id: string; name: string; position: { code: string; name: string } }>;
|
||||||
|
onDone: () => void;
|
||||||
|
onCancel: () => void;
|
||||||
|
}) {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const [draftName, setDraftName] = useState(name);
|
||||||
|
const [draft, setDraft] = useState<Record<string, string>>({ ...members });
|
||||||
|
|
||||||
|
const save = useMutation({
|
||||||
|
mutationFn: (input: { name: string; members: Record<string, StaffId> }) =>
|
||||||
|
call(saveWorkGroupRoute, {
|
||||||
|
body: { ownerId: staffIdSchema.parse(ownerId), name: input.name, members: input.members },
|
||||||
|
}),
|
||||||
|
onSuccess: () => {
|
||||||
|
void queryClient.invalidateQueries({ queryKey: ['gov', 'work-groups'] });
|
||||||
|
onDone();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const bound = Object.values(draft).filter((v) => v !== '').length;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card ico="users" title={`工作组:${ownerName}`}>
|
||||||
|
<Field k="组名" hint="人读的一句话,如「柴浩的工作组」">
|
||||||
|
<input className="g-input" value={draftName} onChange={(e) => setDraftName(e.target.value)} />
|
||||||
|
</Field>
|
||||||
|
{positions.map((p) => (
|
||||||
|
<Field key={p.id} k={`${p.name} · ${p.code}`} hint="留空即不绑定,该岗位仍走四策略">
|
||||||
|
<select
|
||||||
|
className="g-input"
|
||||||
|
value={draft[p.code] ?? ''}
|
||||||
|
onChange={(e) => setDraft({ ...draft, [p.code]: e.target.value })}
|
||||||
|
>
|
||||||
|
<option value="">— 不绑定 —</option>
|
||||||
|
{staff.map((s) => (
|
||||||
|
<option key={s.id} value={s.id}>
|
||||||
|
{s.name}({s.position.code})
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</Field>
|
||||||
|
))}
|
||||||
|
<Acts
|
||||||
|
align="end"
|
||||||
|
items={[
|
||||||
|
{ label: '取消', ico: 'x', onClick: onCancel },
|
||||||
|
{
|
||||||
|
label: save.isPending ? '保存中…' : `保存(已绑 ${bound} 个岗位)`,
|
||||||
|
ico: 'circle-check',
|
||||||
|
primary: true,
|
||||||
|
disabled: save.isPending || draftName.trim() === '',
|
||||||
|
ask: {
|
||||||
|
title: `保存工作组:${draftName}`,
|
||||||
|
note: <Note>绑定先于策略 —— 起单人有组,整条流的岗位就按组指人;没绑定的岗位仍走四策略。</Note>,
|
||||||
|
impact: {
|
||||||
|
who: '仅此后新起的工作流;在途不受影响 —— 绑定是起单时的快照',
|
||||||
|
undo: '可随时改;解绑某岗位即回落四策略',
|
||||||
|
when: '确认后即时生效',
|
||||||
|
},
|
||||||
|
okLabel: '确认保存',
|
||||||
|
onOk: () => {
|
||||||
|
const picked: Record<string, StaffId> = {};
|
||||||
|
for (const [code, id] of Object.entries(draft)) {
|
||||||
|
if (id !== '') picked[code] = staffIdSchema.parse(id);
|
||||||
|
}
|
||||||
|
save.mutate({ name: draftName, members: picked });
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
{save.isError && <Note tone="warn">保存失败,请重试。</Note>}
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import type {
|
|||||||
GovTasksOutput,
|
GovTasksOutput,
|
||||||
GovToolsOutput,
|
GovToolsOutput,
|
||||||
GovTriggersOutput,
|
GovTriggersOutput,
|
||||||
|
GovWorkGroupsOutput,
|
||||||
GovWorkflowOutput,
|
GovWorkflowOutput,
|
||||||
ProcessPackage,
|
ProcessPackage,
|
||||||
} from '@contracts';
|
} from '@contracts';
|
||||||
@@ -41,6 +42,7 @@ import {
|
|||||||
listStaffRoute,
|
listStaffRoute,
|
||||||
listToolsRoute,
|
listToolsRoute,
|
||||||
listTriggersRoute,
|
listTriggersRoute,
|
||||||
|
listWorkGroupsRoute,
|
||||||
validateProcessRoute,
|
validateProcessRoute,
|
||||||
} from '@contracts';
|
} from '@contracts';
|
||||||
import { call } from '@web-shared/typed-fetch.ts';
|
import { call } from '@web-shared/typed-fetch.ts';
|
||||||
@@ -105,6 +107,11 @@ export const useGovPkgReports = (
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export type GovWorkGroups = GovWorkGroupsOutput;
|
||||||
|
|
||||||
|
export const useGovWorkGroups = (): UseQueryResult<GovWorkGroups> =>
|
||||||
|
useQuery({ queryKey: ['gov', 'work-groups'], queryFn: () => call<GovWorkGroups>(listWorkGroupsRoute) });
|
||||||
|
|
||||||
export const useGovTriggers = (): UseQueryResult<GovTriggersOutput> =>
|
export const useGovTriggers = (): UseQueryResult<GovTriggersOutput> =>
|
||||||
useQuery({ queryKey: ['gov', 'triggers'], queryFn: () => call<GovTriggersOutput>(listTriggersRoute) });
|
useQuery({ queryKey: ['gov', 'triggers'], queryFn: () => call<GovTriggersOutput>(listTriggersRoute) });
|
||||||
|
|
||||||
|
|||||||
@@ -156,6 +156,7 @@ export const TABS: Readonly<Record<GovZone, ReadonlyArray<{ key: string; label:
|
|||||||
people: [
|
people: [
|
||||||
{ key: 'emp', label: '员工', ico: 'users' },
|
{ key: 'emp', label: '员工', ico: 'users' },
|
||||||
{ key: 'roles', label: '岗位授权', ico: 'key' },
|
{ key: 'roles', label: '岗位授权', ico: 'key' },
|
||||||
|
{ key: 'groups', label: '工作组', ico: 'users' },
|
||||||
],
|
],
|
||||||
ledgers: [],
|
ledgers: [],
|
||||||
knowledge: [{ key: 'layers', label: '活动记忆', ico: 'layers' }],
|
knowledge: [{ key: 'layers', label: '活动记忆', ico: 'layers' }],
|
||||||
|
|||||||
+2
-2
@@ -27,8 +27,8 @@
|
|||||||
|
|
||||||
## 工作组(ADR-011 落地后的已知欠账)
|
## 工作组(ADR-011 落地后的已知欠账)
|
||||||
|
|
||||||
- **没有治理台管理界面**:建组 / 改组 / 指派默认组只能经播种或直接调用例。
|
- ~~**没有治理台管理界面**~~ —— 2026-07-27 清账:运营中心 · 员工 · 工作组页签可建组 / 改组
|
||||||
`saveWorkGroup` 已在 org 出口就位,缺的是「员工」面下的一个子页。
|
(`GET|PUT /api/gov/work-groups`);实测改绑定后新起工作流的 `work_group` 快照即含新成员表。
|
||||||
- **一人一组**:`workGroupOf` 取该人名下稳定的第一条;起单时选哪个组这一步没做。
|
- **一人一组**:`workGroupOf` 取该人名下稳定的第一条;起单时选哪个组这一步没做。
|
||||||
- **组成员离岗无提醒**:绑定的人离岗时静默退回四策略,不告诉任何人。
|
- **组成员离岗无提醒**:绑定的人离岗时静默退回四策略,不告诉任何人。
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import type { RouteContract } from './http.ts';
|
import type { RouteContract } from './http.ts';
|
||||||
import { disableCheckSchema, grantSchema, positionSchema, staffSchema } from '../org.ts';
|
import { disableCheckSchema, grantSchema, positionSchema, staffSchema, workGroupSchema } from '../org.ts';
|
||||||
import { positionIdSchema, staffIdSchema, taskIdSchema } from '../id.ts';
|
import { positionIdSchema, staffIdSchema, taskIdSchema } from '../id.ts';
|
||||||
import { closureReportSchema, processPackageSchema, triggerConfigSchema } from '../process-package.ts';
|
import { closureReportSchema, processPackageSchema, triggerConfigSchema } from '../process-package.ts';
|
||||||
import { controlSemanticSchema, taskRuntimeStateSchema, terminalStateSchema, workflowStateSchema } from '../terminal.ts';
|
import { controlSemanticSchema, taskRuntimeStateSchema, terminalStateSchema, workflowStateSchema } from '../terminal.ts';
|
||||||
@@ -250,6 +250,34 @@ export const upsertTriggerRoute = {
|
|||||||
output: z.object({ trigger: triggerConfigSchema }),
|
output: z.object({ trigger: triggerConfigSchema }),
|
||||||
} as const satisfies RouteContract;
|
} as const satisfies RouteContract;
|
||||||
|
|
||||||
|
/* ── 员工管理 · 工作组(ADR-011) ───────────────────────────────────── */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 工作组 = 一组「岗位 → 员工」的绑定,**起单时装载、随工作流走**。
|
||||||
|
*
|
||||||
|
* 它是**绑定先于策略**,不是第五种负责人策略:起单人有组,整条流的岗位就按组指人;
|
||||||
|
* 没有组才回落四策略。装进工作流的是**快照** —— 改组不改已起的单。
|
||||||
|
*/
|
||||||
|
export const listWorkGroupsRoute = {
|
||||||
|
method: 'GET',
|
||||||
|
path: '/api/gov/work-groups',
|
||||||
|
/** `ownerName` 是组长姓名的呈现补充,读表时一并带出,免得前端再对一次 id */
|
||||||
|
output: z.object({ groups: z.array(workGroupSchema.extend({ ownerName: z.string() })) }),
|
||||||
|
} as const satisfies RouteContract;
|
||||||
|
|
||||||
|
/** 一人一组:按 `ownerId` 落,已有即更新、没有即新建 */
|
||||||
|
export const saveWorkGroupRoute = {
|
||||||
|
method: 'PUT',
|
||||||
|
path: '/api/gov/work-groups',
|
||||||
|
body: z.object({
|
||||||
|
ownerId: staffIdSchema,
|
||||||
|
name: z.string().min(1),
|
||||||
|
/** 岗位 code → 员工 id;留空的岗位不绑定,那一位仍走四策略 */
|
||||||
|
members: z.record(z.string().min(1), staffIdSchema),
|
||||||
|
}),
|
||||||
|
output: z.object({ group: workGroupSchema }),
|
||||||
|
} as const satisfies RouteContract;
|
||||||
|
|
||||||
/* ── 活动库管理:两维分类 × 包版本 × 资产版本 ─────────────────────────── */
|
/* ── 活动库管理:两维分类 × 包版本 × 资产版本 ─────────────────────────── */
|
||||||
|
|
||||||
export const listActivitiesRoute = {
|
export const listActivitiesRoute = {
|
||||||
@@ -427,6 +455,7 @@ export const govMetricsRoute = {
|
|||||||
/* ── 出参类型:契约自己命名,消费方不再 `z.infer` ──────────────────────── */
|
/* ── 出参类型:契约自己命名,消费方不再 `z.infer` ──────────────────────── */
|
||||||
|
|
||||||
export type GovTasksOutput = z.infer<typeof listGovTasksRoute.output>;
|
export type GovTasksOutput = z.infer<typeof listGovTasksRoute.output>;
|
||||||
|
export type GovWorkGroupsOutput = z.infer<typeof listWorkGroupsRoute.output>;
|
||||||
export type GovApprovalsOutput = z.infer<typeof listApprovalsRoute.output>;
|
export type GovApprovalsOutput = z.infer<typeof listApprovalsRoute.output>;
|
||||||
export type GovStaffOutput = z.infer<typeof listStaffRoute.output>;
|
export type GovStaffOutput = z.infer<typeof listStaffRoute.output>;
|
||||||
export type GovGrantsOutput = z.infer<typeof listGrantsRoute.output>;
|
export type GovGrantsOutput = z.infer<typeof listGrantsRoute.output>;
|
||||||
|
|||||||
Reference in New Issue
Block a user