Archived
feat(w05): 数据齐了也叫人一次 —— S1 改派只读的验收工单,件同一份、编辑钮收起
用户:「就算数据是足的也进入这个数据工单,就派发一个验收工单,界面和数据要和现在的 第一个数据工单一致,只不过把芯片卡片上的编辑按钮去掉,只展示不能编辑,然后把标题旁边的 数据工单改成验收工单」。 病根在引擎拍首那道 doneWhen 扫描:输入本来就齐的单,S1 的 doneWhen 第一拍就为真, 配方一笔都还没跑,这一步当场被收掉 —— 卡永远发不出去,人一眼没看过就进了 S2。 治法:CMMN 给 S1 标 manualActivation,那道扫描多一个门槛(worldTouched),次序变成 「配方发卡 → 人答 → 下一拍 doneWhen 为真才收」;doneWhen 仍是真门,只是不抢在人前面。 平台:执行工单第三副面孔 variant:'accept'(件只读、人只确认「确认无误」)—— 与 mode:'acceptance'(submit 抬起来的收口验收,带打回重做)分清;三副面孔的判断 抽成纯函数 order-face.ts(有测),chat-bubbles 只摆。 活动:S1 两笔互斥的 card(carded 幂等保证一步只发一张)+ 案卷粘着的旗 order_kind; 一张件两副面孔 —— 题头/标题/页脚三处名字与卡角编辑钮按 v-data / v-accept 由样式表挑 (藏的是容器 .fabs,逐枚去盖打不完)。 测:引擎测钉死那一对(标了人工激活 → 卡发得出去、步不收;没标 → 步当场被收、卡发不出); auto-runner variant 透传;order-face 4 条;包内 2 条(两笔卡 + 粘旗)、view 1 条; 两处变异验证。e2e w05 5/5 released、被拒 0、模型调用恰 1 次 —— 都在库那例摆的正是验收工单。 2558 测、四门禁绿;2.15.0 装库核过。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LKMZDwMbyKzjw6Hembwvet
This commit is contained in:
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* 工单穿哪副面孔(2026-08-31 用户定「数据齐了也叫人一次,派验收工单」逼出的第三副)。
|
||||
*
|
||||
* 三副面孔都是**执行工单**的变体,回传即办结;收口验收单(`mode:'acceptance'`)是另一回事,
|
||||
* 它有自己的裁决表单(AcceptForm)—— `faceOf` 对它返回 undefined,别顶着执行工单的牌子摆出来。
|
||||
*/
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { ACCEPT_FACE, DATA_FACE, EXEC_FACE, chipOf, faceOf } from '../internal/order-face.ts';
|
||||
|
||||
describe('faceOf —— 没办结的执行工单穿哪副', () => {
|
||||
it('不写变体 = 执行工单;data = 数据工单;accept = 验收工单', () => {
|
||||
expect(faceOf('execution', undefined)).toBe(EXEC_FACE);
|
||||
expect(faceOf('execution', 'data')).toBe(DATA_FACE);
|
||||
expect(faceOf('execution', 'accept')).toBe(ACCEPT_FACE);
|
||||
});
|
||||
|
||||
it('收口验收单不走这儿 —— 它是 submit 抬起来的裁决,不是「这一步要你办的事」', () => {
|
||||
expect(faceOf('acceptance', undefined)).toBeUndefined();
|
||||
});
|
||||
|
||||
it('三副面孔的四字与按钮字各不相同 —— 人一眼要能认出这张卡要他干什么', () => {
|
||||
const faces = [EXEC_FACE, DATA_FACE, ACCEPT_FACE];
|
||||
expect(new Set(faces.map((f) => f.docTitle)).size).toBe(3);
|
||||
expect(new Set(faces.map((f) => f.submit)).size).toBe(3);
|
||||
expect(new Set(faces.map((f) => f.klass)).size).toBe(3);
|
||||
/* 验收工单的件是只读的,按钮说的就得是「确认」而不是「提交/写入」 */
|
||||
expect(ACCEPT_FACE.submit).toBe('确认无误');
|
||||
expect(ACCEPT_FACE.docHint).toContain('只读');
|
||||
});
|
||||
});
|
||||
|
||||
describe('chipOf —— 已办结 / 没有指导书的那张小卡', () => {
|
||||
it('数据工单摆「数据」,验收工单与收口验收单同摆「验收」,其余「执行」', () => {
|
||||
expect(chipOf('execution', 'data')).toEqual({ klass: 't-data', ico: 'database', label: '数据' });
|
||||
expect(chipOf('execution', 'accept')).toEqual({ klass: 't-acc', ico: 'shield-check', label: '验收' });
|
||||
expect(chipOf('acceptance', undefined)).toEqual({ klass: 't-acc', ico: 'shield-check', label: '验收' });
|
||||
expect(chipOf('execution', undefined)).toEqual({ klass: 't-exe', ico: 'clipboard-check', label: '执行' });
|
||||
});
|
||||
});
|
||||
@@ -21,6 +21,8 @@ import { EVENT_TYPE_LABEL, isRejection } from '@contracts';
|
||||
import { Ico } from '@web-shared/Ico.tsx';
|
||||
import { Btn } from '@web-shared/Btn.tsx';
|
||||
import { approveReply, checklistMissing, redoReply, reviewChoices } from './review-face.ts';
|
||||
import { ACCEPT_FACE, DATA_FACE, EXEC_FACE, chipOf } from './order-face.ts';
|
||||
import type { FormFace } from './order-face.ts';
|
||||
import type { DecisionMessage } from './review-face.ts';
|
||||
import { ChatAvatar } from '@web-shared/ChatAvatar.tsx';
|
||||
import { atClock } from '@web-shared/at.ts';
|
||||
@@ -359,6 +361,15 @@ function Msg({
|
||||
onLocate={onLocate}
|
||||
{...(orderValues === undefined ? {} : { orderValues })}
|
||||
/>
|
||||
) : order.mode === 'execution' && order.variant === 'accept' && !settled ? (
|
||||
/* 验收工单 = 执行工单的变体(契约 `variant`):件只读,人只确认 */
|
||||
<AcceptCheckForm
|
||||
order={order}
|
||||
onReplyOrder={onReplyOrder}
|
||||
onLocate={onLocate}
|
||||
onField={onField}
|
||||
{...(orderValues === undefined ? {} : { orderValues })}
|
||||
/>
|
||||
) : order.mode === 'execution' && order.variant === 'data' && !settled ? (
|
||||
/* 数据工单 = 执行工单的变体(契约 `variant`):同一套办法,另一副面孔 */
|
||||
<DataForm
|
||||
@@ -765,13 +776,13 @@ function Kv({ items }: { items: ReadonlyArray<{ k: string; v: string }> }) {
|
||||
function WoCard({ order, onLocate }: { order: WorkOrder; onLocate: (id: string) => void }) {
|
||||
const result = order.done?.result;
|
||||
const bad = result !== undefined && isRejection(result);
|
||||
const exe = order.mode === 'execution';
|
||||
const data = order.mode === 'execution' && order.variant === 'data';
|
||||
/* 三副面孔 + 收口验收单,哪一副由纯函数说了算(`order-face.ts`,有测)—— 这里只摆 */
|
||||
const chip = chipOf(order.mode, order.mode === 'execution' ? order.variant : undefined);
|
||||
return (
|
||||
<div className={'jc-fc ' + (data ? 't-data' : exe ? 't-exe' : 't-acc')}>
|
||||
<div className={'jc-fc ' + chip.klass}>
|
||||
<div className="jc-fc-h">
|
||||
<Ico name={data ? 'database' : exe ? 'file-text' : 'shield-check'} s={13} />
|
||||
<b>工单 · {data ? '数据' : exe ? '执行' : '验收'}</b>
|
||||
<Ico name={chip.ico} s={13} />
|
||||
<b>工单 · {chip.label}</b>
|
||||
<em className={result === undefined ? '' : bad ? 'bad' : 'ok'}>
|
||||
{result === undefined ? '待处理' : bad ? '已退回' : '已办结'}
|
||||
</em>
|
||||
@@ -962,40 +973,6 @@ function Guide({ steps, standard }: { steps?: readonly string[] | undefined; sta
|
||||
* 表格那一份值不在这儿收 —— 人是在工作区的件上改的,由外层缓冲带过来
|
||||
* (与验收裁决同一条路子:编辑与提交分在两个区,缓冲得由壳层持有)。
|
||||
*/
|
||||
/** 执行工单与数据工单是同一套办法、两副面孔 —— 面孔在这儿定,办法只写一份 */
|
||||
type FormFace = {
|
||||
readonly klass: 't-exe' | 't-data';
|
||||
readonly headIcon: 'clipboard-check' | 'database';
|
||||
readonly head: string;
|
||||
readonly docIcon: 'file-text' | 'table';
|
||||
readonly docTitle: string;
|
||||
readonly docHint: string;
|
||||
readonly submit: string;
|
||||
};
|
||||
const EXEC_FACE: FormFace = {
|
||||
klass: 't-exe',
|
||||
headIcon: 'clipboard-check',
|
||||
head: '工单 · 执行',
|
||||
docIcon: 'file-text',
|
||||
docTitle: '执行工单',
|
||||
docHint: '整份清单在工作区,每一格都能就地改;改完回这里提交',
|
||||
submit: '提交',
|
||||
};
|
||||
/**
|
||||
* 数据工单(2026-08-27 用户:「工单类型新加入了一类数据工单…照着这个大概的样式为数据工单单独做一个组件」):
|
||||
* 让人**从零建一张主数据表** —— 件上的表就是要建的记录,填齐、保存,回这里确认即入库,活动从「等待数据」唤醒。
|
||||
* 引擎那一侧与执行工单无异(契约 `ExecutionWorkOrder.variant = 'data'`),所以只换面孔:色、图标、四字、按钮字。
|
||||
*/
|
||||
const DATA_FACE: FormFace = {
|
||||
klass: 't-data',
|
||||
headIcon: 'database',
|
||||
head: '工单 · 数据',
|
||||
docIcon: 'table',
|
||||
docTitle: '数据工单',
|
||||
docHint: '件上的表就是要建的记录:每格就地填、保存;填齐后回这里确认,确认即入库',
|
||||
submit: '确认写入',
|
||||
};
|
||||
|
||||
type ExecFormProps = {
|
||||
order: Extract<WorkOrder, { mode: 'execution' }>;
|
||||
onReplyOrder: (reply: WorkOrderReply) => void;
|
||||
@@ -1009,6 +986,11 @@ function DataForm(props: ExecFormProps) {
|
||||
return <ExecForm {...props} face={DATA_FACE} />;
|
||||
}
|
||||
|
||||
/** 验收工单卡 —— 执行工单的第三副面孔(见 ACCEPT_FACE) */
|
||||
function AcceptCheckForm(props: ExecFormProps) {
|
||||
return <ExecForm {...props} face={ACCEPT_FACE} />;
|
||||
}
|
||||
|
||||
function ExecForm({ order, onReplyOrder, onLocate, orderValues, onField, face = EXEC_FACE }: ExecFormProps & { face?: FormFace }) {
|
||||
const busy = useReplyBusy();
|
||||
const val = (f: FormField) => orderValues?.[f.key] ?? f.ai ?? '';
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
/**
|
||||
* 工单穿哪副面孔 —— 纯函数,零 React(与 `review-face.ts` 同一条理由:**判断要能钉测**)。
|
||||
*
|
||||
* 执行工单一套办法、**三副面孔**(契约 `ExecutionWorkOrder.variant`):
|
||||
* · 不写 → 执行工单:作业指导书 + 件上就地办,「提交」;
|
||||
* · `data` → 数据工单(2026-08-27 用户定):件上的表就是要建的记录,「确认写入」即入库;
|
||||
* · `accept` → 验收工单(2026-08-31 用户定):件只读,人只核对,「确认无误」。
|
||||
*
|
||||
* **`accept` 与 `mode:'acceptance'` 不是一回事**:后者是 `submit` 抬起来的收口验收
|
||||
* (签下去就落账发口,带打回重做那一路,对话区走 `AcceptForm`),这里的三副面孔都只是
|
||||
* 「这一步要你办的事」—— 回传即办结,放行不了任何东西。
|
||||
*/
|
||||
import type { WorkOrderMode } from '@contracts';
|
||||
|
||||
export type FormFace = {
|
||||
readonly klass: 't-exe' | 't-data' | 't-acc';
|
||||
readonly headIcon: 'clipboard-check' | 'database' | 'shield-check';
|
||||
readonly head: string;
|
||||
readonly docIcon: 'file-text' | 'table';
|
||||
readonly docTitle: string;
|
||||
readonly docHint: string;
|
||||
readonly submit: string;
|
||||
};
|
||||
|
||||
export const EXEC_FACE: FormFace = {
|
||||
klass: 't-exe',
|
||||
headIcon: 'clipboard-check',
|
||||
head: '工单 · 执行',
|
||||
docIcon: 'file-text',
|
||||
docTitle: '执行工单',
|
||||
docHint: '整份清单在工作区,每一格都能就地改;改完回这里提交',
|
||||
submit: '提交',
|
||||
};
|
||||
|
||||
/**
|
||||
* 数据工单(2026-08-27 用户:「工单类型新加入了一类数据工单…照着这个大概的样式为数据工单单独做一个组件」):
|
||||
* 让人**从零建一张主数据表** —— 件上的表就是要建的记录,填齐、保存,回这里确认即入库,活动从「等待数据」唤醒。
|
||||
*/
|
||||
export const DATA_FACE: FormFace = {
|
||||
klass: 't-data',
|
||||
headIcon: 'database',
|
||||
head: '工单 · 数据',
|
||||
docIcon: 'table',
|
||||
docTitle: '数据工单',
|
||||
docHint: '件上的表就是要建的记录:每格就地填、保存;填齐后回这里确认,确认即入库',
|
||||
submit: '确认写入',
|
||||
};
|
||||
|
||||
/**
|
||||
* 验收工单(2026-08-31 用户定:「就算数据是足的也进入这个数据工单,就派发一个验收工单…
|
||||
* 把芯片卡片上的编辑按钮去掉,只展示不能编辑」):这一步的活已经齐了,件只读,人过目、确认。
|
||||
*/
|
||||
export const ACCEPT_FACE: FormFace = {
|
||||
klass: 't-acc',
|
||||
headIcon: 'shield-check',
|
||||
head: '工单 · 验收',
|
||||
docIcon: 'file-text',
|
||||
docTitle: '验收工单',
|
||||
docHint: '整份内容在工作区,只读不可改;核对无误后回这里确认',
|
||||
submit: '确认无误',
|
||||
};
|
||||
|
||||
/** 一张**没办结的执行工单**该穿哪副面孔;收口验收单(mode: 'acceptance')不走这儿 —— 它有自己的裁决表单 */
|
||||
export function faceOf(mode: WorkOrderMode, variant: 'data' | 'accept' | undefined): FormFace | undefined {
|
||||
if (mode !== 'execution') return undefined;
|
||||
if (variant === 'data') return DATA_FACE;
|
||||
if (variant === 'accept') return ACCEPT_FACE;
|
||||
return EXEC_FACE;
|
||||
}
|
||||
|
||||
/**
|
||||
* 已办结的、以及没有作业指导书的那张小卡上的三样东西。
|
||||
* **验收工单与收口验收单在这张小卡上同一副样子** —— 人读的都是「验收」,
|
||||
* 至于签下去会不会落账,不是这张小卡要答的问题。
|
||||
*/
|
||||
export function chipOf(
|
||||
mode: WorkOrderMode,
|
||||
variant: 'data' | 'accept' | undefined,
|
||||
): { readonly klass: FormFace['klass']; readonly ico: FormFace['headIcon']; readonly label: string } {
|
||||
const face = mode === 'execution' && variant === 'data' ? DATA_FACE : mode === 'execution' && variant !== 'accept' ? EXEC_FACE : ACCEPT_FACE;
|
||||
return { klass: face.klass, ico: face.headIcon, label: face.head.replace('工单 · ', '') };
|
||||
}
|
||||
Reference in New Issue
Block a user