/**
* feature `gov-task` —— 任务管理:在途监控、控制事件(暂停/叫停/终止)、恢复、改派、任务日志查看
*
* 自 `~/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 {
CONTROL_SEMANTIC_LABEL,
TASK_RUNTIME_STATE_LABEL,
TERMINAL_STATE_LABEL,
WORKFLOW_STATE_LABEL,
controlTaskRoute,
reassignTaskRoute,
resumeTaskRoute,
} from '@contracts';
import type { ControlSemantic } from '@contracts';
import { Ico } from '@web-shared/Ico.tsx';
import { call } from '@web-shared/typed-fetch.ts';
import { Ava, Card, Chip, Clear, Entry, Facts, Kpi, NList, NRow, Note, PageHead, QueryGate, Sec, St } from '@web-shared/gov/primitives.tsx';
import type { ActSpec } from '@web-shared/gov/primitives.tsx';
import { APR_ST, RUNTIME_ST, TERMINAL_ST, WF_ST } from '@web-shared/gov/tone.ts';
import type { Nav } from '@web-shared/gov/gov-route.ts';
import { FlowFigure } from '@web-shared/gov/flow/FlowFigure.tsx';
import { atFull, atMinute } from '@web-shared/at.ts';
import {
useGovApprovals,
useGovStaff,
useGovTaskLog,
useGovTasks,
useGovWorkflow,
} from '@web-shared/gov/gov-api.ts';
export type HomeProps = { tab: string; nav: Nav };
export function TasksHome({ tab, nav }: HomeProps) {
return ;
}
/**
* 在途工作流看板 —— **工作流是流程包的运行时**,一级看板归「运营中心 · 工作流」区;
* 任务区(活动的运行时)不再摆它。
*/
export function WorkflowBoard({ nav }: { nav: Nav }) {
const q = useGovTasks();
const tasks = q.data?.tasks ?? [];
const workflows = q.data?.workflows ?? [];
/** 工作流不设负责人:要看「这条流现在落在谁身上」,读它当前节点的任务负责人 */
const holderOf = (workflowId: string) => {
const live = tasks.find((t) => t.workflowId === workflowId && t.terminal === undefined);
if (live) return { name: live.ownerName, label: '当前任务负责人' };
const stuck = tasks.find((t) => t.workflowId === workflowId && t.terminal !== undefined && t.terminal !== 'released');
return stuck ? { name: stuck.ownerName, label: '最后经手 · 待处置' } : undefined;
};
const lanes = [
{ k: 'running', tone: 'brand' },
{ k: 'waiting', tone: 'warn' },
{ k: 'abnormal', tone: 'danger' },
{ k: 'completed', tone: 'ok' },
] as const;
return (
{lanes.map((ln) => {
const items = workflows.filter((w) => w.state === ln.k);
return (
{WORKFLOW_STATE_LABEL[ln.k]}
{items.length}
{items.map((w) => {
const mine = tasks.filter((t) => t.workflowId === w.id);
const done = mine.filter((t) => t.terminal === 'released').length;
const total = Math.max(mine.length, 1);
const holder = holderOf(w.id);
const cur = mine.find((t) => t.terminal === undefined)?.title ?? '—';
return (
);
})}
{items.length === 0 &&
—
}
);
})}
);
}
function RuntimeHome({ tab, nav }: HomeProps) {
const q = useGovTasks();
const approvals = useGovApprovals();
const tasks = q.data?.tasks ?? [];
const workflows = q.data?.workflows ?? [];
if (tab === 'apr') {
const rows = approvals.data?.approvals ?? [];
return (
<>
审批点是流程层的通用人裁节点,与活动、网关、触发器、流程包并列为编排层五要素——它是
系统内置要素,声明即用、零开发、不进活动库。它卡到审批人工位产出审批单:通过 /
驳回,附依据;没有预算、没有 Harness、没有任务日志,所以不是任务。
{rows.map((a) => (
{APR_ST[a.state].label}
}
onClick={() => nav.push({ k: 'wf', id: a.workflowId })}
/>
))}
{rows.length === 0 && 暂无审批单——流上还没有走到审批点。}
>
);
}
if (tab === 'list') {
return (
<>
{tasks.map((t) => {
const st = t.terminal !== undefined ? TERMINAL_ST[t.terminal] : RUNTIME_ST[t.runtime];
const label = t.terminal !== undefined ? TERMINAL_STATE_LABEL[t.terminal] : TASK_RUNTIME_STATE_LABEL[t.runtime];
return (
{t.usedSteps}/{t.capSteps}
),
},
{ k: '起始', v: {atMinute(t.startedAt)} },
]}
right={
{label}
}
onClick={() => nav.push({ k: 'task', id: t.id })}
/>
);
})}
{tasks.length === 0 && 暂无任务——起单后这里会出现。}
>
);
}
const abnormal = workflows.filter((w) => w.state === 'abnormal');
const stuck = tasks.filter((t) => t.terminal === 'blocked' || t.terminal === 'exhausted' || t.terminal === 'stalled');
return (
w.state === 'running' || w.state === 'waiting').length}
sub="运行中 + 等待中"
/>
t.terminal === 'released').length}
sub="唯一成功终态"
/>
{abnormal.map((w) => (
nav.push({ k: 'wf', id: w.id })}
/>
))}
{stuck.map((t) => (
nav.push({ k: 'task', id: t.id })}
/>
))}
{abnormal.length + stuck.length === 0 && 无待处置——在途实例全部正常推进。}
);
}
/* ---------------- ② 员工管理 ---------------- */
/** 治理写动作统一入口:成功即失效相关查询,画面由服务端重投影 */
function useGovAction(mutate: (input: TInput) => Promise, keys: readonly string[]) {
const queryClient = useQueryClient();
return useMutation({
mutationFn: mutate,
onSuccess: () => {
for (const key of keys) void queryClient.invalidateQueries({ queryKey: ['gov', key] });
},
});
}
/* ---------------- 工作流 ---------------- */
export function WfPage({ id, nav }: { id: string; nav: Nav }) {
const workflowId = id.includes(':') ? id.split(':')[0]! : id;
const q = useGovWorkflow(workflowId);
if (q.data === undefined) return 载入中…;
const { workflow, pkg, instances, tasks } = q.data;
const st = WF_ST[workflow.state];
return (
<>
{WORKFLOW_STATE_LABEL[workflow.state]}
{workflow.packageId} v{workflow.packageVersion}
来源 · {workflow.source.kind} · {workflow.source.ref}
>
}
desc="工作流状态由它上面的节点(任务与审批单)聚合推导,工作流层不另设状态机;重开产生新的一条工作流,旧的已终、不再入算。"
/>
{tasks.map((t) => (
nav.push({ k: 'task', id: t.id })}
/>
))}
{tasks.length === 0 && 暂无任务。}
{instances.map((i) => (
{pkg.nodes.find((n) => n.id === i.nodeId)?.label ?? i.nodeId}
{i.instance > 1 ? ` #${i.instance}` : ''}
{i.terminal !== undefined
? TERMINAL_STATE_LABEL[i.terminal]
: i.running
? '运行中'
: i.waiting
? '等待中'
: i.activated
? '已激活'
: '未激活'}
))}
>
);
}
/* ---------------- 任务:在途监控 · 控制 · 改派 ---------------- */
export function TaskPage({ id, nav }: { id: string; nav: Nav }) {
const q = useGovTasks();
const staff = useGovStaff();
const task = (q.data?.tasks ?? []).find((t) => t.id === id);
const [assignee, setAssignee] = useState('');
const control = useGovAction(
(semantic: ControlSemantic) =>
call(controlTaskRoute, { params: { taskId: id }, body: { semantic, reason: '治理台干预' } }),
['tasks', 'workflow', 'tasklog'],
);
const resume = useGovAction(() => call(resumeTaskRoute, { params: { taskId: id } }), ['tasks', 'workflow', 'tasklog']);
const reassign = useGovAction(
(to: string) => call(reassignTaskRoute, { params: { taskId: id }, body: { to } }),
['tasks', 'staff'],
);
if (task === undefined) return 任务不存在或已不在近期窗口内。;
const st = task.terminal !== undefined ? TERMINAL_ST[task.terminal] : RUNTIME_ST[task.runtime];
const label = task.terminal !== undefined ? TERMINAL_STATE_LABEL[task.terminal] : TASK_RUNTIME_STATE_LABEL[task.runtime];
const live = task.terminal === undefined;
const suspended = task.runtime === 'suspended';
/** 控制三语义:暂停可恢复、叫停归负责人、终止级联作废 */
const controlAct = (semantic: ControlSemantic, danger: boolean): ActSpec => ({
label: CONTROL_SEMANTIC_LABEL[semantic],
ico: semantic === 'terminate' ? 'power' : semantic === 'halt' ? 'hand' : 'pause',
danger,
disabled: !live,
ask: {
title: `控制事件 · ${CONTROL_SEMANTIC_LABEL[semantic]}:${task.title}`,
note: 控制事件走统一事件信封(四类事件之控制事件),先持久后投递。,
impact: {
who:
semantic === 'terminate'
? '在途任务终止、待答卡与征答消息作废、审批点撤卡(级联)'
: semantic === 'halt'
? `负责人 ${task.ownerName} —— 挂起并记最重档纠正信号`
: '任务挂起,监听与预算冻结',
undo: semantic === 'terminate' ? '不可恢复' : '可恢复',
when: '确认后即时',
},
okLabel: `确认${CONTROL_SEMANTIC_LABEL[semantic]}`,
onOk: () => control.mutate(semantic),
},
});
return (
<>
{label}
{task.ownerName}
>
}
desc="活动的一次运行 —— 治理不改定义:控制只作用于这一次运行。"
acts={[
controlAct('pause', false),
controlAct('halt', true),
controlAct('terminate', true),
{
label: '恢复',
ico: 'play',
primary: true,
disabled: !suspended,
ask: {
title: `恢复任务:${task.title}`,
note: 恢复 = 从挂起点继续;预算与监听解冻,任务记忆只追加、不回滚。,
impact: { who: `负责人 ${task.ownerName}`, undo: '可再次控制', when: '确认后即时' },
okLabel: '确认恢复',
onOk: () => resume.mutate(undefined),
},
},
]}
/>
{`${task.usedSteps}/${task.capSteps} 步`}}>
新负责人
{(staff.data?.staff ?? [])
.filter((s) => s.state === 'active' && s.name !== task.ownerName)
.map((s) => (
))}
nav.push({ k: 'tasklog', id: task.id })}
/>
>
);
}
export function TaskLogPage({ id }: { id: string }) {
const q = useGovTaskLog(id);
const lines = q.data?.log ?? [];
return (
<>
{lines.map((l) => (
{l.title}
{l.at.slice(0, 19).replace('T', ' ')}
{/* 与工作台同口径:章是四字动作;归侧另有度量面在看 */}
{l.action}
))}
{lines.length === 0 &&
暂无留痕。}
>
);
}
/* ---------------- 员工 / 岗位 ---------------- */