完善报销通知与付款进度(08-01-reimbursement-notification-progress-clarity)
这个提交包含在:
@@ -18,6 +18,10 @@ export function resolveNotificationTargetLink(item: NotificationTarget) {
|
||||
return `/reimbursements/${item.targetId}`;
|
||||
}
|
||||
|
||||
if (item.targetType === "payment_batch") {
|
||||
return `/payment-batches?id=${encodeSegment(item.targetId)}`;
|
||||
}
|
||||
|
||||
if (item.targetType === "leave_request") {
|
||||
return `/leave-requests?highlight=${item.targetId}`;
|
||||
}
|
||||
@@ -60,6 +64,10 @@ export function resolveMobileNotificationTargetLink(item: NotificationTarget) {
|
||||
return `/mobile/reimbursements/${encodeSegment(item.targetId)}`;
|
||||
}
|
||||
|
||||
if (item.targetType === "payment_batch") {
|
||||
return `/payment-batches?id=${encodeSegment(item.targetId)}`;
|
||||
}
|
||||
|
||||
if (item.targetType === "leave_request") {
|
||||
return `/mobile/leave/${encodeSegment(item.targetId)}`;
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ import { Spinner } from "../components/ui/spinner";
|
||||
import { Tabs, TabsList, TabsTrigger } from "../components/ui/tabs";
|
||||
import { Textarea } from "../components/ui/textarea";
|
||||
import { MOBILE_APPROVAL_PERMISSIONS } from "../features/mobile/navigation";
|
||||
import { resolvePurchaseDisplayCode } from "../lib/approvalDisplay";
|
||||
import { resolvePurchaseDisplayCode, workflowActionLabel } from "../lib/approvalDisplay";
|
||||
import { businessPrimaryLabel, compactBusinessReference, joinBusinessMeta } from "../lib/businessDisplay";
|
||||
import { resolveReimbursementDisplayCode } from "../lib/reimbursementDisplay";
|
||||
import { cn } from "../lib/utils";
|
||||
@@ -150,27 +150,6 @@ function workflowStatusLabel(status: string) {
|
||||
return status;
|
||||
}
|
||||
|
||||
function workflowActionLabel(action: string | null | undefined) {
|
||||
if (!action) {
|
||||
return "-";
|
||||
}
|
||||
|
||||
const labels: Record<string, string> = {
|
||||
submit: "提交",
|
||||
approve: "通过",
|
||||
reject: "驳回",
|
||||
return: "退回",
|
||||
transfer: "转审",
|
||||
withdraw: "撤回",
|
||||
remind: "催办",
|
||||
overdue_remind: "超时提醒",
|
||||
cancel: "取消",
|
||||
skip: "跳过",
|
||||
};
|
||||
|
||||
return labels[action] ?? action;
|
||||
}
|
||||
|
||||
function businessStatusLabel(status: string | null | undefined) {
|
||||
const labels: Record<string, string> = {
|
||||
draft: "草稿",
|
||||
|
||||
@@ -17,7 +17,7 @@ const PAGE_SIZE = 20;
|
||||
const NOTIFICATION_TYPE_OPTIONS = [
|
||||
{ label: "全部", value: "" },
|
||||
{ label: "审批事件", value: "workflow.task_assigned,workflow.task_reminded,workflow.task_overdue" },
|
||||
{ label: "报销", value: "reimbursement.approved,reimbursement.rejected,reimbursement.returned,reimbursement.paid" },
|
||||
{ label: "报销", value: "reimbursement.approved,reimbursement.rejected,reimbursement.returned,reimbursement.paid,payment_batch.review_requested,payment_batch.execution_requested" },
|
||||
{ label: "请假", value: "leave.approved,leave.rejected" },
|
||||
{ label: "采购", value: "purchase_request.approved,purchase_request.rejected" },
|
||||
{ label: "差旅", value: "travel_request.approved,travel_request.rejected" },
|
||||
@@ -39,6 +39,7 @@ function formatTime(value: string) {
|
||||
function notificationTypeLabel(type: string) {
|
||||
if (type.startsWith("workflow.")) return "审批事件";
|
||||
if (type.startsWith("reimbursement.")) return "报销事件";
|
||||
if (type.startsWith("payment_batch.")) return "付款事件";
|
||||
if (type.startsWith("leave.")) return "请假事件";
|
||||
if (type.startsWith("purchase_request.")) return "采购事件";
|
||||
if (type.startsWith("travel_request.")) return "差旅事件";
|
||||
|
||||
@@ -17,7 +17,7 @@ const PAGE_SIZE = 20;
|
||||
const NOTIFICATION_TYPE_OPTIONS = [
|
||||
{ label: "全部", value: "" },
|
||||
{ label: "审批提醒", value: "workflow.task_assigned" },
|
||||
{ label: "报销", value: "reimbursement.approved,reimbursement.rejected,reimbursement.returned,reimbursement.paid" },
|
||||
{ label: "报销", value: "reimbursement.approved,reimbursement.rejected,reimbursement.returned,reimbursement.paid,payment_batch.review_requested,payment_batch.execution_requested" },
|
||||
{ label: "请假", value: "leave.approved,leave.rejected" },
|
||||
{ label: "采购", value: "purchase_request.approved,purchase_request.rejected" },
|
||||
{ label: "差旅", value: "travel_request.approved,travel_request.rejected" },
|
||||
|
||||
@@ -32,7 +32,7 @@ import { PageScaffold } from "../components/patterns/PageScaffold";
|
||||
import { SectionPanel } from "../components/patterns/SectionPanel";
|
||||
import { Button } from "../components/ui/button";
|
||||
import { Textarea } from "../components/ui/textarea";
|
||||
import { resolvePurchaseDisplayCode } from "../lib/approvalDisplay";
|
||||
import { resolvePurchaseDisplayCode, workflowActionLabel } from "../lib/approvalDisplay";
|
||||
import { formatDisplayDateTime } from "../lib/displayFormat";
|
||||
import {
|
||||
buildPcDetailSource,
|
||||
@@ -85,27 +85,6 @@ function workflowInstanceStatusLabel(status: string | undefined | null) {
|
||||
return labels[status] ?? status;
|
||||
}
|
||||
|
||||
function workflowActionLabel(action: string | undefined | null) {
|
||||
if (!action) {
|
||||
return "-";
|
||||
}
|
||||
|
||||
const labels: Record<string, string> = {
|
||||
submit: "提交",
|
||||
approve: "通过",
|
||||
reject: "驳回",
|
||||
return: "退回",
|
||||
transfer: "转审",
|
||||
withdraw: "撤回",
|
||||
remind: "催办",
|
||||
overdue_remind: "超时提醒",
|
||||
cancel: "取消",
|
||||
skip: "跳过",
|
||||
};
|
||||
|
||||
return labels[action] ?? action;
|
||||
}
|
||||
|
||||
function DetailField({ label, value }: { label: string; value: ReactNode }) {
|
||||
return (
|
||||
<div className="detail-fact-card">
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { mkdirSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { expect, test, type Browser, type Page, type Route } from "@playwright/test";
|
||||
import {
|
||||
resolveMobileNotificationTargetLink,
|
||||
resolveNotificationTargetLink,
|
||||
} from "../../src/lib/notificationLinks";
|
||||
|
||||
const ROOT_DIR = path.resolve(process.cwd(), "..");
|
||||
const SCREENSHOT_DIR = path.join(ROOT_DIR, "output/playwright/mobile-ia-flow-remediation");
|
||||
@@ -881,6 +885,13 @@ async function expectCommonActionsDoNotDuplicateBottomTabs(page: Page) {
|
||||
}
|
||||
|
||||
test.describe("MIF7 移动信息架构整改", () => {
|
||||
test("付款责任通知在 PC 与移动通知中心都可直达付款批次", () => {
|
||||
const target = { targetType: "payment_batch", targetId: "batch/dg7" };
|
||||
|
||||
expect(resolveNotificationTargetLink(target)).toBe("/payment-batches?id=batch%2Fdg7");
|
||||
expect(resolveMobileNotificationTargetLink(target)).toBe("/payment-batches?id=batch%2Fdg7");
|
||||
});
|
||||
|
||||
test.skip(process.env.E2E_MODE === "live", "MIF7 本地专项使用 mock 数据验证信息架构。");
|
||||
|
||||
test("首页常用入口不重复底部 Tab,员工进度页按状态分标签", async ({ browser }) => {
|
||||
@@ -1108,7 +1119,7 @@ test.describe("MIF7 移动信息架构整改", () => {
|
||||
[
|
||||
"mobile-reimbursement-items",
|
||||
"mobile-reimbursement-attachments",
|
||||
"mobile-reimbursement-approval-history",
|
||||
"mobile-reimbursement-processing-progress",
|
||||
].map((testId) => document.querySelector(`[data-testid='${testId}']`)?.getBoundingClientRect().top ?? -1),
|
||||
);
|
||||
expect(sectionOrder[0]).toBeLessThan(sectionOrder[1]);
|
||||
|
||||
@@ -155,8 +155,8 @@ const reimbursementDetail = {
|
||||
{ key: "manager_approval", label: "主管审批", state: "current", actorName: visualMe.name, occurredAt: "2026-05-27T09:00:00.000Z", result: "处理中", comment: null, detail: "待主管审批处理", targetHref: null },
|
||||
{ key: "approval_extra-review", label: "负责人复核", state: "pending", actorName: null, occurredAt: null, result: null, comment: null, detail: "待处理", targetHref: null },
|
||||
{ key: "finance_review", label: "财务复核", state: "pending", actorName: null, occurredAt: null, result: null, comment: null, detail: "待处理", targetHref: null },
|
||||
{ key: "payment_batch_review", label: "付款批次复核", state: "pending", actorName: null, occurredAt: null, result: null, comment: null, detail: "待财务创建付款批次", targetHref: null },
|
||||
{ key: "payment_and_receipt", label: "付款与回单", state: "pending", actorName: null, occurredAt: null, result: null, comment: null, detail: "待付款批次复核后处理", targetHref: null },
|
||||
{ key: "payment_batch_review", label: "付款复核", state: "pending", actorName: null, occurredAt: null, result: null, comment: null, detail: "待财务安排付款", targetHref: null },
|
||||
{ key: "payment_and_receipt", label: "付款完成", state: "pending", actorName: null, occurredAt: null, result: null, comment: null, detail: "待付款复核后由财务登记,非审批环节", targetHref: null },
|
||||
],
|
||||
},
|
||||
approvalHistory: [
|
||||
@@ -1101,11 +1101,12 @@ test("报销详情固定使用上海时区并收敛只读信息", async ({ page
|
||||
await expect(page.getByTestId("reimbursement-materials").getByRole("button", { name: /查看补充附件/ })).toHaveCount(1);
|
||||
await expect(page.getByTestId("reimbursement-materials").getByRole("button", { name: /移除|删除/ })).toHaveCount(0);
|
||||
await expect(page.getByTestId("reimbursement-workflow-progress")).toContainText("负责人复核");
|
||||
await expect(page.getByTestId("reimbursement-workflow-progress")).toContainText("付款批次复核");
|
||||
await expect(page.getByTestId("reimbursement-workflow-progress")).toContainText("付款复核");
|
||||
await expect(page.getByTestId("reimbursement-workflow-progress")).not.toContainText(/第 \d+ 步/);
|
||||
|
||||
await page.goto("/workflow/tasks/task-wave3");
|
||||
await expect(page.getByTestId("workflow-task-overview")).toContainText("2026年5月27日 17:05");
|
||||
await expect(page.getByText(`提交 · ${visualMe.name}`)).toBeVisible();
|
||||
|
||||
await page.setViewportSize({ width: 390, height: 844 });
|
||||
await page.goto("/mobile/reimbursements/reim-wave3");
|
||||
@@ -1119,7 +1120,7 @@ test("报销详情固定使用上海时区并收敛只读信息", async ({ page
|
||||
await expect(page.getByTestId("mobile-reimbursement-materials").getByRole("button", { name: /查看发票原件/ })).toHaveCount(1);
|
||||
await expect(page.getByTestId("mobile-reimbursement-materials").getByRole("button", { name: /查看补充附件/ })).toHaveCount(1);
|
||||
await expect(page.getByTestId("mobile-reimbursement-materials").getByRole("button", { name: /移除|删除/ })).toHaveCount(0);
|
||||
await expect(page.getByTestId("mobile-reimbursement-processing-progress")).toContainText("付款与回单");
|
||||
await expect(page.getByTestId("mobile-reimbursement-processing-progress")).toContainText("付款完成");
|
||||
await expect(page.getByTestId("mobile-reimbursement-actions")).toHaveCount(0);
|
||||
await expect(page.getByText("PDF/JPG/PNG/WebP,单次最多 5 个")).toHaveCount(0);
|
||||
await expectNoHorizontalOverflow(page);
|
||||
|
||||
@@ -203,8 +203,8 @@ function buildReimbursementDetail(state: ReturnType<typeof buildReimbursementSta
|
||||
{ key: "submitted", label: "申请提交", state: "current", actorName: "测试员工", occurredAt: null, result: "待提交", comment: null, detail: "待申请人提交", targetHref: null },
|
||||
{ key: "manager_approval", label: "主管审批", state: "pending", actorName: null, occurredAt: null, result: null, comment: null, detail: "待处理", targetHref: null },
|
||||
{ key: "finance_review", label: "财务复核", state: "pending", actorName: null, occurredAt: null, result: null, comment: null, detail: "待处理", targetHref: null },
|
||||
{ key: "payment_batch_review", label: "付款批次复核", state: "pending", actorName: null, occurredAt: null, result: null, comment: null, detail: "待财务创建付款批次", targetHref: null },
|
||||
{ key: "payment_and_receipt", label: "付款与回单", state: "pending", actorName: null, occurredAt: null, result: null, comment: null, detail: "待付款批次复核后处理", targetHref: null },
|
||||
{ key: "payment_batch_review", label: "付款复核", state: "pending", actorName: null, occurredAt: null, result: null, comment: null, detail: "待财务安排付款", targetHref: null },
|
||||
{ key: "payment_and_receipt", label: "付款完成", state: "pending", actorName: null, occurredAt: null, result: null, comment: null, detail: "待付款复核后由财务登记,非审批环节", targetHref: null },
|
||||
],
|
||||
},
|
||||
approvalHistory: [],
|
||||
|
||||
@@ -71,8 +71,9 @@ test("E2E 报销处理进度:提交后按真实 workflow 展示职责进度",
|
||||
await expect(reimbursementDetail.workflowProgress).toContainText("申请提交");
|
||||
await expect(reimbursementDetail.workflowProgress).toContainText("审批");
|
||||
await expect(reimbursementDetail.workflowProgress).not.toContainText("审批节点1");
|
||||
await expect(reimbursementDetail.workflowProgress).toContainText("付款批次复核");
|
||||
await expect(reimbursementDetail.workflowProgress).toContainText("付款与回单");
|
||||
await expect(reimbursementDetail.workflowProgress).toContainText("付款复核");
|
||||
await expect(reimbursementDetail.workflowProgress).toContainText("付款完成");
|
||||
await expect(reimbursementDetail.workflowProgress).toContainText("非审批环节");
|
||||
await expect(reimbursementDetail.workflowProgress).not.toContainText(/第 \d+ 步/);
|
||||
|
||||
await logout(page, EMPLOYEE.name);
|
||||
|
||||
在新工单中引用
屏蔽一个用户