/** * feature `gov-staff` —— 员工管理:账号、岗位授权、停用前置检查 * * 自 `~/coding/JCAI_PRD/governance/_components/Governance.tsx` 平移: * 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 { Acts, Card, Chip, 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 { useGovGrants, useGovStaff, useGovTasks, useGovWorkGroups } from '@web-shared/gov/gov-api.ts'; export type HomeProps = { tab: string; nav: Nav }; export function PeopleHome({ tab, nav }: HomeProps) { if (tab === 'groups') return ; return ; } function StaffHome({ tab, nav }: HomeProps) { const q = useGovStaff(); const tasks = useGovTasks(); const staff = q.data?.staff ?? []; const positions = q.data?.positions ?? []; if (tab === 'roles') { return ( <> 权限按岗位授予,不按人:客体三类(数据 / 工具 / 治理功能)× 动作三种(读 / 写 / 治理)。 人换岗即换权,权限随岗位走。 {positions.map((p) => ( s.position.id === p.id).length }]} onClick={() => nav.push({ k: 'role', id: p.id })} /> ))} ); } return ( <> 停用前置检查:在途任务与待答卡须先改派完毕——停用不得让在途工作无人认领。 {staff.map((s) => { const mine = (tasks.data?.tasks ?? []).filter((t) => t.ownerName === s.name && t.terminal === undefined); return ( {s.state === 'active' ? '在职' : '已停用'} } onClick={() => nav.push({ k: 'emp', id: s.id })} /> ); })} ); } /* ---------------- ③ 工作流管理 ---------------- */ export function EmpPage({ id, nav }: { id: string; nav: Nav }) { const q = useGovStaff(); const tasks = useGovTasks(); const s = (q.data?.staff ?? []).find((x) => x.id === id); if (s === undefined) return 员工不存在。; const mine = (tasks.data?.tasks ?? []).filter((t) => t.ownerName === s.name); const live = mine.filter((t) => t.terminal === undefined); return ( <> {s.position.name} · {s.position.code} {s.state === 'active' ? '在职' : '已停用'} } desc="停用前置检查:在途任务与待答卡须先改派完毕——停用不得让在途工作无人认领。" /> {live.length}}> {live.map((t) => ( nav.push({ k: 'task', id: t.id })} /> ))} {live.length === 0 && 无在途任务——可安全停用。} nav.push({ k: 'role', id: s.position.id })} /> ); } export function RolePage({ id }: { id: string }) { const staff = useGovStaff(); const grants = useGovGrants(id); const position = (staff.data?.positions ?? []).find((p) => p.id === id); return ( <> {position?.code ?? '—'}} desc="权限按岗位授予,不按人:客体三类(数据 / 工具 / 治理功能)× 动作三种(读 / 写 / 治理)。人换岗即换权。" />
{(grants.data?.grants ?? []).map((g, i) => (
{g.object.kind === 'data' ? `数据 · ${g.object.domain}.${g.object.object}` : g.object.kind === 'governance' ? `治理功能 · ${GOVERNANCE_FUNCTION_LABEL[g.object.function]}` : `任务 · ${g.object.activityId ?? '全部活动'}`} {GRANT_ACTION_LABEL[g.action]}
))} {(grants.data?.grants ?? []).length === 0 && 未授予任何权限。}
); } /* ---------------- 流程包 ---------------- */ /* ---------------- 工作组(ADR-011) ---------------- */ /** * 工作组 = 一组「岗位 → 员工」的绑定,**起单时装载、随工作流走**。 * * 它是**绑定先于策略**,不是第五种负责人策略:起单人有组,整条流的岗位就按组指人; * 没有组才回落四策略(独任 / 轮询 / 抢单 / 指派)。 * 装进工作流的是**快照** —— 改组不改已起的单,留痕不被改写。 */ function WorkGroupsTab() { const groups = useGovWorkGroups(); const staffQ = useGovStaff(); const [editing, setEditing] = useState(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 ( <> 工作组把岗位绑到人:谁起的单,整条流的岗位就按谁的组指人 —— 绑定先于策略, 没绑定的岗位仍走四策略。绑定在起单那一刻快照进工作流,此后改组不影响已起的单。 {rows.length === 0 ? ( 还没有工作组 —— 给谁建一个,他起的单整条流就能一次指到人。 ) : ( rows.map((g) => editing === g.ownerId ? ( setEditing(undefined)} onCancel={() => setEditing(undefined)} /> ) : ( setEditing(g.ownerId)}> 改绑定 } > ({ k: `${p.name} · ${p.code}`, v: g.members[p.code] === undefined ? '未绑定 · 走四策略' : nameOf(g.members[p.code]!), })), ]} /> ), ) )} {staff .filter((s) => !rows.some((g) => g.ownerId === s.id)) .map((s) => editing === s.id ? null : ( 建组 →} onClick={() => setEditing(s.id)} /> ), )} {staff.every((s) => rows.some((g) => g.ownerId === s.id)) && 每个人都有组了。} {staff .filter((s) => !rows.some((g) => g.ownerId === s.id) && editing === s.id) .map((s) => ( setEditing(undefined)} onCancel={() => setEditing(undefined)} /> ))} ); } /** 建组 / 改组表单:逐岗位选人,留空即该岗位不绑定 */ function WorkGroupForm({ ownerId, ownerName, name, members, positions, staff, onDone, onCancel, }: { ownerId: string; ownerName: string; name: string; members: Readonly>; 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>({ ...members }); const save = useMutation({ mutationFn: (input: { name: string; members: Record }) => 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 ( setDraftName(e.target.value)} /> {positions.map((p) => ( ))} 绑定先于策略 —— 起单人有组,整条流的岗位就按组指人;没绑定的岗位仍走四策略。, impact: { who: '仅此后新起的工作流;在途不受影响 —— 绑定是起单时的快照', undo: '可随时改;解绑某岗位即回落四策略', when: '确认后即时生效', }, okLabel: '确认保存', onOk: () => { const picked: Record = {}; for (const [code, id] of Object.entries(draft)) { if (id !== '') picked[code] = staffIdSchema.parse(id); } save.mutate({ name: draftName, members: picked }); }, }, }, ]} /> {save.isError && 保存失败,请重试。} ); }