forked from ChaiHao/BPOS-MVP
四个 desk-* feature 自 workbench 原型 ModuleWorkspace.tsx 平移(jc-* 类名原样),
共用件落 shared/desk/module-kit;mock 数据换成服务端投影:
· desk-approval:审批点卡到审批人工位;依据快照、驳回走向(声明式)、驳回必附意见
· desk-message:通知 + 跨任务征答**就地作答**;卡面表单由 card.ts schema 驱动,
与后端回传校验同 schema;跨人卡不占任务清单,不开放任务对话流
· desk-minute:纪要维护 + 决议登记起单;正文可续、决议只增
· desk-report:四周期草案 + 度量八指标 + 确认即提交
左轨角标常驻(待批 / 未读),责任面要一眼看见什么在等我。
修一个真 bug:process.start 从不校验流程包的 callInterface,
先插工作流再由 task.start 撞活动契约 —— 失败就留下一条永远推不动的在途单。
改为**先校验后建单**(CallInterfaceViolationError),一条工作流都不留;
路由回 400 并带上缺哪个字段。已清掉这次触发出的两条孤儿单。
判据 ⑩ 纪要一环跑通:决议 → 登记触发 → 工作流 → A1 落到 BD 冯兵,
登记人即来源已记在单上;同一决议重复登记被 412 挡下。
gates: tsc / eslint / depcruise(316 模块)/ api-report / 380 测试 全绿
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KeDBx2PHLavbURXB8LJJ7m
182 lines
7.8 KiB
TypeScript
182 lines
7.8 KiB
TypeScript
/**
|
|
* 汇报要素 —— 自 `~/coding/JCAI_PRD/workbench/_components/ModuleWorkspace.tsx`
|
|
* 的 `ReportModule` 平移;mock 数据换成服务端投影。
|
|
*
|
|
* 「按日 / 周 / 月 / 年**自动**总结此员工的工作——由本人各任务的**任务日志**聚合生成,
|
|
* 确认即提交,**零手工汇报**。」结构化部分零 LLM(事件流骨架 + 归侧分栏)。
|
|
*/
|
|
import { useState } from 'react';
|
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
|
import type { ReportPeriodName, ReportView } from '@contracts';
|
|
import { REPORT_PERIOD_LABEL, draftReportRoute, submitReportRoute } from '@contracts';
|
|
import { Ico } from '@web-shared/Ico.tsx';
|
|
import ScrollArea from '@web-shared/ScrollArea.tsx';
|
|
import { call } from '@web-shared/typed-fetch.ts';
|
|
import { Chip, DocPanel, Hero, ListEmpty, NoteBand, Section, SidebarHead } from '@web-shared/desk/module-kit.tsx';
|
|
|
|
const PERIODS: ReadonlyArray<{ k: ReportPeriodName; ico: string }> = [
|
|
{ k: 'day', ico: 'calendar-days' },
|
|
{ k: 'week', ico: 'calendar-clock' },
|
|
{ k: 'month', ico: 'calendar-days' },
|
|
{ k: 'year', ico: 'history' },
|
|
];
|
|
|
|
const pct = (v: number): string => `${Math.round(v * 100)}%`;
|
|
|
|
export function ReportDesk() {
|
|
const queryClient = useQueryClient();
|
|
const [period, setPeriod] = useState<ReportPeriodName>('day');
|
|
const [note, setNote] = useState('');
|
|
|
|
const q = useQuery({
|
|
queryKey: ['report', period],
|
|
queryFn: () => call<ReportView>(draftReportRoute, { params: { period } }),
|
|
});
|
|
const report = q.data;
|
|
const submitted = report?.submittedAt !== undefined;
|
|
|
|
const submit = useMutation({
|
|
mutationFn: () =>
|
|
call<ReportView>(submitReportRoute, {
|
|
params: { reportId: report!.id },
|
|
body: note.trim() === '' ? {} : { note: note.trim() },
|
|
}),
|
|
onSuccess: () => void queryClient.invalidateQueries({ queryKey: ['report'] }),
|
|
});
|
|
|
|
return (
|
|
<div className="workspace">
|
|
<aside className="task-sidebar">
|
|
<SidebarHead icon="bar-chart" title="工作汇报" sub="任务日志聚合 · 零手工汇报" />
|
|
<div className="jc-flt-wrap" aria-label="汇报周期">
|
|
<div className="jc-flt" role="tablist" aria-label="按周期">
|
|
{PERIODS.map((p) => (
|
|
<button key={p.k} className={period === p.k ? 'active' : ''} onClick={() => setPeriod(p.k)}>
|
|
<span>{REPORT_PERIOD_LABEL[p.k]}</span>
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
<ScrollArea className="jc-sa--fill" vpClassName="task-list">
|
|
{report !== undefined && (
|
|
<button className="jc-mcard selected">
|
|
<span className="jc-mcard-datebox">
|
|
<b>{report.window.from.slice(8, 10)}</b>
|
|
<i>{report.window.from.slice(5, 7)} 月</i>
|
|
</span>
|
|
<span className="jc-mcard-body">
|
|
<span className="jc-mcard-top">
|
|
<em>{report.title}</em>
|
|
<Chip tone={submitted ? 'ok' : 'todo'} label={submitted ? '已提交' : '待确认'} pulse={!submitted} />
|
|
</span>
|
|
<strong className="jc-mcard-title jc-wrap">
|
|
经手 {report.counts.touched} 单 · 放行 {report.counts.released} 单
|
|
</strong>
|
|
<span className="jc-mcard-foot">
|
|
<Ico name="git-branch" s={12} />
|
|
{report.counts.logLines} 条动作
|
|
<i>系统生成</i>
|
|
</span>
|
|
</span>
|
|
</button>
|
|
)}
|
|
</ScrollArea>
|
|
</aside>
|
|
|
|
{report === undefined ? (
|
|
<main className="execution-panel jc-panel-card">
|
|
<ListEmpty icon="bar-chart">正在聚合…</ListEmpty>
|
|
</main>
|
|
) : (
|
|
<DocPanel scrollKey={`${period}:${report.id}`}>
|
|
<Hero
|
|
grad={['#0aa294', '#0a6a64']}
|
|
icon="bar-chart"
|
|
kicker={`汇报 · ${REPORT_PERIOD_LABEL[period]}`}
|
|
title={report.title}
|
|
meta={
|
|
<>
|
|
<Ico name="bot" s={14} />
|
|
<b>系统自动生成</b>
|
|
<span className="jc-dim">由本人各任务的任务日志聚合</span>
|
|
</>
|
|
}
|
|
chip={<Chip tone={submitted ? 'ok' : 'todo'} label={submitted ? '已提交' : '待确认'} pulse={!submitted} />}
|
|
/>
|
|
|
|
<Section icon="bar-chart" title="度量八指标(零埋点 · 口径挂在归侧标签上)" extra={`共 ${report.counts.logLines} 条日志`}>
|
|
<div className="jc-stats">
|
|
{[
|
|
{ ico: 'bot', label: '自主率', v: pct(report.metrics.autonomy) },
|
|
{ ico: 'rotate-ccw', label: '纠正率', v: pct(report.metrics.correction) },
|
|
{ ico: 'gauge', label: '有效自主率', v: pct(report.metrics.effectiveAutonomy) },
|
|
{ ico: 'hand', label: '打扰率', v: report.metrics.interruption.toFixed(1) },
|
|
{ ico: 'git-commit', label: 'DMN 覆盖率', v: pct(report.metrics.dmnCoverage) },
|
|
{ ico: 'circle-dot', label: '断点率', v: pct(report.metrics.breakpoint) },
|
|
].map((s) => (
|
|
<div key={s.label} className="jc-stat">
|
|
<span className="jc-stat-ico">
|
|
<Ico name={s.ico} s={15} />
|
|
</span>
|
|
<span className="jc-stat-v">{s.v}</span>
|
|
<span className="jc-stat-k">{s.label}</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
<NoteBand>
|
|
自主率 = 机器产出 ÷(机器产出 + 人产出),裁决与等待不进分母;它与纠正率
|
|
<b>成对读</b>,复合为有效自主率 —— 单看任一都会被优化坏。
|
|
单位成本 {Math.round(report.metrics.unitCost.tokens)} token /
|
|
{report.metrics.unitCost.minutes.toFixed(1)} 分钟每放行单,分母只认放行通过。
|
|
</NoteBand>
|
|
</Section>
|
|
|
|
<Section icon="clock" title="动作明细(结构化日志 · 零 LLM)" extra={`${report.sections.length} 单任务`}>
|
|
{report.sections.map((s) => (
|
|
<div key={s.taskId} className="jc-axis">
|
|
<div className="jc-sec-head">
|
|
<Ico name="list-checks" s={13} />
|
|
<span>{s.title}</span>
|
|
{s.terminalLabel !== undefined && <span className="jc-sec-extra">{s.terminalLabel}</span>}
|
|
</div>
|
|
{s.lines.map((line, i) => (
|
|
<div key={i} className="jc-axis-row">
|
|
<span className="jc-axis-node" />
|
|
<p>{line}</p>
|
|
</div>
|
|
))}
|
|
</div>
|
|
))}
|
|
{report.sections.length === 0 && <span className="jc-dim">这个周期内没有动作</span>}
|
|
</Section>
|
|
|
|
<Section icon="pen-line" title="员工补充" extra="可选 · 系统不要求手工写汇报">
|
|
<textarea
|
|
className="jc-sup"
|
|
rows={3}
|
|
disabled={submitted}
|
|
placeholder="补充主观判断或下阶段计划(可留空)"
|
|
value={submitted ? (report.note ?? '') : note}
|
|
onChange={(e) => setNote(e.target.value)}
|
|
/>
|
|
</Section>
|
|
|
|
<div className="jc-mod-actions">
|
|
{submitted ? (
|
|
<span className="jc-mod-donehint">
|
|
<Ico name="circle-check" s={15} />
|
|
已确认提交 · 汇报进入组织视图
|
|
</span>
|
|
) : (
|
|
<button type="button" className="card-btn primary" disabled={submit.isPending} onClick={() => submit.mutate()}>
|
|
<Ico name="check" s={13} />
|
|
确认提交
|
|
</button>
|
|
)}
|
|
</div>
|
|
</DocPanel>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|