实现 B-2a 报销审批内容工作台(reimbursement-approval-content-workbench)

这个提交包含在:
2026-09-01 20:32:34 +08:00
父节点 4c8ee07abb
当前提交 c840574d36
共修改 36 个文件,包含 2271 行新增和 91 行删除
@@ -110,7 +110,11 @@ function navigationItem(key: string, href: string, label: string, icon: string,
return { key, href, label, icon, placement, permissionCodes: [], featureFlagKey: null };
}
async function mockWorkflowConfirmApis(page: Page) {
async function mockWorkflowConfirmApis(page: Page, options: {
taskDetail?: typeof pendingTaskDetail | Record<string, unknown>;
reimbursementDetail?: Record<string, unknown>;
reimbursementDetailFailure?: boolean;
} = {}) {
const actionCalls = {
approve: 0,
reject: 0,
@@ -248,6 +252,19 @@ async function mockWorkflowConfirmApis(page: Page) {
return;
}
if (/^\/api\/reimbursements\/[^/]+$/.test(path)) {
if (options.reimbursementDetailFailure) {
await route.fulfill({
status: 500,
contentType: "application/json",
body: JSON.stringify({ requestId: "mock-workflow-confirm", error: { code: "SYS_001", message: "合成加载失败", details: null } }),
});
} else {
await fulfillJson(route, options.reimbursementDetail ?? {});
}
return;
}
const actionMatch = path.match(/^\/api\/workflow\/tasks\/[^/]+\/(approve|reject|return)$/);
if (actionMatch) {
const action = actionMatch[1] as keyof typeof actionCalls;
@@ -257,7 +274,7 @@ async function mockWorkflowConfirmApis(page: Page) {
}
if (/^\/api\/workflow\/tasks\/[^/]+$/.test(path)) {
await fulfillJson(route, pendingTaskDetail);
await fulfillJson(route, options.taskDetail ?? pendingTaskDetail);
return;
}
@@ -341,4 +358,101 @@ test.describe("R7E 审批动作二次确认", () => {
await page.getByTestId("workflow-action-confirm-submit").click();
await expect.poll(() => actionCalls.return).toBe(1);
});
test("PC 报销审批同屏展示证据、引用退回意见且不暴露维护动作", async ({ page }) => {
const reimbursementId = "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb";
const taskDetail = {
...pendingTaskDetail,
workflowInstance: {
...pendingTaskDetail.workflowInstance,
entityType: "reimbursement",
contextData: {
standardViolations: [{
itemId: "item-1", expenseTypeId: "type-1", expenseTypeName: "交通费", amount: "180.00",
standardId: "standard-1", standardName: "交通费标准", maxAmount: "120.00", perUnit: "item",
controlMode: "warn", scopeType: "global", scopeKey: null, exceeded: true, exceededAmount: "60.00",
}],
budgetCheck: null,
budgetCheckStatus: "missing",
parseStatus: "ready",
},
},
reimbursement: {
id: reimbursementId,
formNumber: "BX-APPROVAL-001",
status: "reviewing",
reimbursementDate: "2026-09-01",
totalAmount: "180.00",
description: "客户拜访交通费",
owner: { id: "owner", employeeNo: "E001", name: "申请人" },
submitter: { id: "owner", employeeNo: "E001", name: "申请人" },
},
leaveRequest: null,
};
const reimbursementDetail = {
id: reimbursementId,
items: [{
id: "item-1", expenseTypeId: "type-1", expenseDate: "2026-09-01", amount: "180.00",
description: "客户拜访打车", assetPriorApprovalBasis: null,
expenseType: { id: "type-1", code: "travel_taxi", name: "交通费", category: "travel", enabled: true, requiresInvoice: true },
}],
invoices: [
{
id: "invoice-1", status: "bound", invoiceCode: null, invoiceNumber: "FP-001", invoiceDate: "2026-09-01",
sellerName: "合成供应商", buyerName: null, documentCategory: "invoice", documentSubtype: null,
totalAmount: "120.00", originalName: "invoice.pdf", mimeType: "application/pdf", fileSize: 100,
linkedItemId: "item-1", amountEvidenceStatus: "verified",
},
{
id: "invoice-2", status: "bound", invoiceCode: null, invoiceNumber: "FP-002", invoiceDate: "2026-09-01",
sellerName: "合成供应商", buyerName: null, documentCategory: "invoice", documentSubtype: null,
totalAmount: null, originalName: "unlinked.pdf", mimeType: "application/pdf", fileSize: 100,
linkedItemId: null, amountEvidenceStatus: "manual_review",
},
],
attachments: [{ id: "attachment-1", originalName: "说明.pdf", mimeType: "application/pdf", fileSize: 100, createdAt: pendingTaskCreatedAt }],
};
await mockWorkflowConfirmApis(page, { taskDetail, reimbursementDetail });
await login(page, MANAGER);
await page.goto(`/workflow/tasks/${pendingTask.id}`);
await expect(page.getByTestId("reimbursement-approval-evidence")).toContainText("客户拜访打车");
await expect(page.getByTestId("approval-unlinked-invoices")).toContainText("FP-002");
await expect(page.getByTestId("reimbursement-approval-checks")).toContainText("该次提交未记录预算快照");
await expect(page.getByText("当前申报金额高于已读取的票面金额合计")).toBeVisible();
await expect(page.getByRole("button", { name: "编辑明细" })).toHaveCount(0);
await expect(page.getByRole("button", { name: "删除明细" })).toHaveCount(0);
await expect(page.getByRole("button", { name: "选择明细" })).toHaveCount(0);
await page.getByTestId("approval-reference-item").click();
await expect(page.getByTestId("workflow-task-return-comment")).toHaveValue(/【明细:交通费 · 2026-09-01 · ¥180\.00】/);
});
test("PC 报销内容加载失败时四类审批入口 fail closed", async ({ page }) => {
const taskDetail = {
...pendingTaskDetail,
workflowInstance: {
...pendingTaskDetail.workflowInstance,
entityType: "reimbursement",
contextData: { standardViolations: [], budgetCheck: null, budgetCheckStatus: "not_applicable", parseStatus: "ready" },
},
reimbursement: {
id: "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb", formNumber: "BX-FAIL-001", status: "reviewing",
reimbursementDate: "2026-09-01", totalAmount: "100.00", description: "合成失败场景",
owner: { id: "owner", employeeNo: "E001", name: "申请人" }, submitter: { id: "owner", employeeNo: "E001", name: "申请人" },
},
leaveRequest: null,
};
await mockWorkflowConfirmApis(page, { taskDetail, reimbursementDetailFailure: true });
await login(page, MANAGER);
await page.goto(`/workflow/tasks/${pendingTask.id}`);
await expect(page.getByRole("alert")).toContainText("服务暂不可用");
await expect(page.getByTestId("workflow-task-approve")).toBeDisabled();
await expect(page.getByTestId("workflow-task-return-toggle")).toBeDisabled();
await expect(page.getByTestId("workflow-task-reject-toggle")).toBeDisabled();
await page.locator("summary", { hasText: "更多" }).click();
await expect(page.getByTestId("workflow-task-transfer-menu-item")).toBeDisabled();
});
});