From 279be83931f7113d3a71b2267dade2dd7d4c5925 Mon Sep 17 00:00:00 2001 From: shunli Date: Tue, 24 Mar 2026 08:58:36 +0800 Subject: [PATCH] =?UTF-8?q?R31-1=20R31-4:=20=E6=94=B6=E5=8F=A3=E8=AF=A6?= =?UTF-8?q?=E6=83=85=E9=A1=B5=E5=B7=A5=E4=BD=9C=E5=8F=B0=E5=8C=96=E4=B8=8E?= =?UTF-8?q?=20E2E=20=E5=A5=91=E7=BA=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/lib/testIds.ts | 51 ++ client/src/pages/ReimbursementDetailPage.tsx | 349 ++++++++------ client/src/pages/TodosPage.tsx | 15 +- client/src/pages/WeeklyReportsPage.tsx | 27 +- client/src/pages/WorkflowTaskDetailPage.tsx | 441 ++++++++++-------- client/src/pages/WorkspaceSetupPage.tsx | 10 +- client/tests/e2e/helpers/auth.ts | 7 + client/tests/e2e/helpers/fixtures/index.ts | 23 + .../tests/e2e/helpers/pageObjects/AppShell.ts | 45 ++ .../pageObjects/ReimbursementDetailPage.ts | 66 +++ .../pageObjects/WorkflowTaskDetailPage.ts | 66 +++ client/tests/e2e/helpers/testIds.ts | 21 +- client/tests/e2e/leave-flow.spec.ts | 65 +-- client/tests/e2e/main-flow.spec.ts | 59 +-- .../e2e/mobile-approvals-offline.spec.ts | 23 +- client/tests/e2e/settings-governance.spec.ts | 30 +- client/tests/e2e/weekly-todo-flow.spec.ts | 63 +++ 17 files changed, 906 insertions(+), 455 deletions(-) create mode 100644 client/src/lib/testIds.ts create mode 100644 client/tests/e2e/helpers/fixtures/index.ts create mode 100644 client/tests/e2e/helpers/pageObjects/AppShell.ts create mode 100644 client/tests/e2e/helpers/pageObjects/ReimbursementDetailPage.ts create mode 100644 client/tests/e2e/helpers/pageObjects/WorkflowTaskDetailPage.ts create mode 100644 client/tests/e2e/weekly-todo-flow.spec.ts diff --git a/client/src/lib/testIds.ts b/client/src/lib/testIds.ts new file mode 100644 index 00000000..23a1e91e --- /dev/null +++ b/client/src/lib/testIds.ts @@ -0,0 +1,51 @@ +export const TEST_IDS = { + login: { + identifier: "login-identifier", + password: "login-password", + submit: "login-submit", + }, + topbar: { + profileTrigger: "topbar-profile-trigger", + }, + workflow: { + tasksTable: "workflow-tasks-table", + taskRow: "workflow-task-row", + detailLink: "workflow-task-detail-link", + summary: "workflow-task-summary", + approve: "workflow-task-approve", + reject: "workflow-task-reject", + rejectComment: "workflow-task-reject-comment", + returnBtn: "workflow-task-return", + returnComment: "workflow-task-return-comment", + transferApprover: "workflow-task-transfer-approver", + transferReason: "workflow-task-transfer-reason", + transferBtn: "workflow-task-transfer", + actionError: "workflow-task-action-error", + overview: "workflow-task-overview", + }, + reimbursement: { + summary: "reimbursement-summary", + items: "reimbursement-items", + itemAdd: "reimbursement-item-add", + submit: "reimbursement-submit", + withdraw: "reimbursement-withdraw", + pay: "reimbursement-pay", + budgetHint: "reimbursement-budget-hint", + erpExport: "reimbursement-erp-export", + overview: "reimbursement-overview", + }, + weeklyReport: { + save: "weekly-report-save", + submit: "weekly-report-submit", + createTodo: "weekly-report-create-todo", + }, + todo: { + createTitle: "todo-create-title", + createSubmit: "todo-create-submit", + row: "todo-row", + complete: "todo-complete", + }, + audit: { + export: "audit-export", + }, +} as const; diff --git a/client/src/pages/ReimbursementDetailPage.tsx b/client/src/pages/ReimbursementDetailPage.tsx index df721875..63848e52 100644 --- a/client/src/pages/ReimbursementDetailPage.tsx +++ b/client/src/pages/ReimbursementDetailPage.tsx @@ -23,11 +23,15 @@ import { useAuth } from "../auth/AuthContext"; import { hasPermission } from "../auth/permissions"; import { CardSkeleton } from "../components/common/CardSkeleton"; import { ConfirmDialog } from "../components/common/ConfirmDialog"; -import { PageHeader } from "../components/common/PageHeader"; import { RiskFlagsCard } from "../components/common/RiskFlagsCard"; +import { StatusBadge } from "../components/common/StatusBadge"; import { AiSuggestionCard } from "../components/ai/AiSuggestionCard"; import { Button } from "../components/ui/button"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "../components/ui/table"; +import { DetailSideMeta } from "../components/patterns/DetailSideMeta"; +import { EntityOverviewHero } from "../components/patterns/EntityOverviewHero"; +import { PageScaffold } from "../components/patterns/PageScaffold"; +import { SectionPanel } from "../components/patterns/SectionPanel"; import { ReimbursementActions, type PayPayload } from "./reimbursement-detail/ReimbursementActions"; import { ReimbursementItemEditor, @@ -80,7 +84,7 @@ export function ReimbursementDetailPage() { const erpPreviewQuery = useQuery({ enabled: Boolean(reimbursementId) && canManageIntegration && detailQuery.data?.status === "paid", - queryKey: ["reimbursements", reimbursementId, "erp-journal-preview"], + queryKey: ["reimbursements", reimbursementId, "integration-journal-preview"], queryFn: () => previewReimbursementJournal(reimbursementId), }); @@ -247,7 +251,7 @@ export function ReimbursementDetailPage() { }, onSuccess: async (result) => { await Promise.all([ - queryClient.invalidateQueries({ queryKey: ["reimbursements", reimbursementId, "erp-journal-preview"] }), + queryClient.invalidateQueries({ queryKey: ["reimbursements", reimbursementId, "integration-journal-preview"] }), queryClient.invalidateQueries({ queryKey: ["reimbursements", reimbursementId] }), ]); setActionError(""); @@ -390,128 +394,91 @@ export function ReimbursementDetailPage() { }; return ( -
- + + + 报销日期:{detail.reimbursementDate ?? "-"} + 报销人:{detail.owner?.name ?? "-"} + 提交人:{detail.submitter?.name ?? "-"} + 金额:{detail.totalAmount ?? "-"} + + )} + actions={( - } + )} /> -
-
报销ID:{detail.id}
-
报销日期:{detail.reimbursementDate ?? "-"}
-
金额:{detail.totalAmount ?? "-"}
-
提交人:{detail.submitter?.name ?? "-"}
-
报销人:{detail.owner?.name ?? "-"}
-
说明:{detail.description || "-"}
-
- -
-
-
-

关联项目

-

报销可选挂到项目,用于项目执行面板汇总。

-
- {detail.project ? 查看项目 : null} -
- -
- - {budgetHint ? ( -
-

预算提示

-
预算池:{budgetHint.budgetCode} / {budgetHint.budgetName}
-
- 本次提交后预计占用:{budgetHint.nextUsedAmount} / {budgetHint.allocatedAmount} - ({budgetHint.nextUsagePercent.toFixed(2)}%) -
-
控制模式:{budgetHint.controlMode}
- {budgetHint.exceedsBudget && budgetHint.controlMode === "block" ? ( -

- 当前金额将超出预算上限,提交时会被阻断(block 模式)。 -

- ) : budgetHint.exceedsThreshold ? ( -

- 当前金额已达到预算预警阈值,提交后会记录预算预警审计。 -

- ) : ( -

当前预算充足,可继续提交。

- )} -
- ) : null} - - - {canManageIntegration && detail.status === "paid" ? ( -
-
-
-

财务桥接

-

已付款报销单会按标准凭证切片通过 Outbox 异步发送到财务桥接出口。

-
- -
- - {erpPreviewQuery.isLoading ?

正在加载凭证切片预览...

: null} - {erpPreviewQuery.isError ? ( -

- {isApiError(erpPreviewQuery.error) - ? `${erpPreviewQuery.error.code}: ${mapApiErrorMessage( - erpPreviewQuery.error.code, - erpPreviewQuery.error.details, - erpPreviewQuery.error.message, - )}` - : "加载财务桥接预览失败"} -

- ) : null} - - {erpPreviewQuery.data ? ( -
-
-

凭证切片

-
类型:{erpPreviewQuery.data.journal.journalType}
-
单号:{erpPreviewQuery.data.journal.documentNo ?? "-"}
-
金额:{erpPreviewQuery.data.journal.totalAmount ?? "-"}
-
法体:{erpPreviewQuery.data.journal.dimensions.legalEntityCode ?? "-"}
-
成本中心:{erpPreviewQuery.data.journal.dimensions.costCenterCode ?? "-"}
-
付款凭证号:{erpPreviewQuery.data.journal.paymentVoucherNo ?? "-"}
-
付款日期:{erpPreviewQuery.data.journal.paymentDate ?? "-"}
+
+
+ +
+
+
+
当前状态
+
+
+
+
关联项目
+
{detail.project ? `${detail.project.projectNo} · ${detail.project.name}` : "未关联"}
+
+
+
明细数
+
{detail.items.length}
+
+
+
关联票据
+
{detail.invoices.length}
+
-
-

导出状态

-
外部ID:{erpPreviewQuery.data.exportState.externalId ?? "-"}
-
Outbox:{erpPreviewQuery.data.exportState.lastOutboxId ?? "-"}
-
队列状态:{erpPreviewQuery.data.exportState.lastOutboxStatus ?? "-"}
-
最近导出:{formatDateTime(erpPreviewQuery.data.exportState.exportedAt)}
-
最近错误:{erpPreviewQuery.data.exportState.lastError ?? "-"}
+ +
+
状态:{detail.status}
+
报销ID:{detail.id}
+
报销日期:{detail.reimbursementDate ?? "-"}
+
金额:{detail.totalAmount ?? "-"}
+
提交人:{detail.submitter?.name ?? "-"}
+
报销人:{detail.owner?.name ?? "-"}
+
说明:{detail.description || "-"}
+
+ +
+
+
+

关联项目

+

报销可选挂到项目,用于项目执行面板汇总。

+
+ {detail.project ? ( + + 查看项目 + + ) : null} +
+
- ) : null} -
- ) : null} +
- - - - -
+ +
+

报销明细

@@ -616,34 +583,130 @@ export function ReimbursementDetailPage() { isPending={itemSaveMutation.isPending} /> ) : null} -
- - void handleSubmitAction()} - onWithdraw={(comment) => mutation.mutate({ action: "withdraw", comment })} - onPay={(payload) => mutation.mutate({ action: "pay", payload })} - /> - -
-

关联票据

- {(detail.invoices ?? []).length ? ( -
- {detail.invoices.map((item) => ( -
-
{item.id}
-
- 状态 {item.status} / 发票 {item.invoiceCode || "-"}-{item.invoiceNumber || "-"} -
- ))} -
- ) : ( -

暂无关联票据

- )} + +
+

关联票据

+ {(detail.invoices ?? []).length ? ( +
+ {detail.invoices.map((item) => ( +
+
{item.id}
+
+ 状态 {item.status} / 发票 {item.invoiceCode || "-"}-{item.invoiceNumber || "-"} +
+
+ ))} +
+ ) : ( +

暂无关联票据

+ )} +
+
+ + + +
+ {budgetHint ? ( +
+

预算提示

+
预算池:{budgetHint.budgetCode} / {budgetHint.budgetName}
+
+ 本次提交后预计占用:{budgetHint.nextUsedAmount} / {budgetHint.allocatedAmount} + ({budgetHint.nextUsagePercent.toFixed(2)}%) +
+
控制模式:{budgetHint.controlMode}
+ {budgetHint.exceedsBudget && budgetHint.controlMode === "block" ? ( +

+ 当前金额将超出预算上限,提交时会被阻断(block 模式)。 +

+ ) : budgetHint.exceedsThreshold ? ( +

+ 当前金额已达到预算预警阈值,提交后会记录预算预警审计。 +

+ ) : ( +

当前预算充足,可继续提交。

+ )} +
+ ) : null} + + + + + {canManageIntegration && detail.status === "paid" ? ( +
+
+
+

财务桥接

+

已付款报销单会按标准凭证切片通过 Outbox 异步发送到财务桥接出口。

+
+ +
+ + {erpPreviewQuery.isLoading ?

正在加载凭证切片预览...

: null} + {erpPreviewQuery.isError ? ( +

+ {isApiError(erpPreviewQuery.error) + ? `${erpPreviewQuery.error.code}: ${mapApiErrorMessage( + erpPreviewQuery.error.code, + erpPreviewQuery.error.details, + erpPreviewQuery.error.message, + )}` + : "加载财务桥接预览失败"} +

+ ) : null} + + {erpPreviewQuery.data ? ( +
+
+

凭证切片

+
类型:{erpPreviewQuery.data.journal.journalType}
+
单号:{erpPreviewQuery.data.journal.documentNo ?? "-"}
+
金额:{erpPreviewQuery.data.journal.totalAmount ?? "-"}
+
法体:{erpPreviewQuery.data.journal.dimensions.legalEntityCode ?? "-"}
+
成本中心:{erpPreviewQuery.data.journal.dimensions.costCenterCode ?? "-"}
+
付款凭证号:{erpPreviewQuery.data.journal.paymentVoucherNo ?? "-"}
+
付款日期:{erpPreviewQuery.data.journal.paymentDate ?? "-"}
+
+
+

导出状态

+
外部ID:{erpPreviewQuery.data.exportState.externalId ?? "-"}
+
Outbox:{erpPreviewQuery.data.exportState.lastOutboxId ?? "-"}
+
队列状态:{erpPreviewQuery.data.exportState.lastOutboxStatus ?? "-"}
+
最近导出:{formatDateTime(erpPreviewQuery.data.exportState.exportedAt)}
+
最近错误:{erpPreviewQuery.data.exportState.lastError ?? "-"}
+
+
+ ) : null} +
+ ) : null} +
+
+
+ +
+ +
报销编号:{detail.formNumber ?? detail.id}
+
报销人:{detail.owner?.name ?? "-"}
+
提交人:{detail.submitter?.name ?? "-"}
+
当前审批:{detail.pendingWorkflow?.task?.step_name ?? "-"}
+
明细数:{detail.items.length}
+
票据数:{detail.invoices.length}
+
关联项目:{detail.project ? `${detail.project.projectNo} · ${detail.project.name}` : "未关联"}
+
+ + void handleSubmitAction()} + onWithdraw={(comment) => mutation.mutate({ action: "withdraw", comment })} + onPay={(payload) => mutation.mutate({ action: "pay", payload })} + /> +
{ itemDeleteMutation.mutate(confirmDeleteItemId); }} /> -
+ ); } diff --git a/client/src/pages/TodosPage.tsx b/client/src/pages/TodosPage.tsx index f8a28f1f..acfc1af7 100644 --- a/client/src/pages/TodosPage.tsx +++ b/client/src/pages/TodosPage.tsx @@ -12,6 +12,7 @@ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from ". import { toast } from "sonner"; import { isApiError } from "../api/types"; import { mapApiErrorMessage } from "../api/errorMessages"; +import { TEST_IDS } from "../lib/testIds"; function errorMessage(error: unknown, fallback: string) { if (isApiError(error)) { @@ -140,7 +141,7 @@ export function TodosPage() { ) : todoItems.length ? ( todoItems.map((item) => ( - +
{item.title}
{item.description ?? "-"}
@@ -151,7 +152,13 @@ export function TodosPage() { {item.status}
-
diff --git a/client/src/pages/WeeklyReportsPage.tsx b/client/src/pages/WeeklyReportsPage.tsx index 5d339774..838562d6 100644 --- a/client/src/pages/WeeklyReportsPage.tsx +++ b/client/src/pages/WeeklyReportsPage.tsx @@ -15,6 +15,7 @@ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from ". import { toast } from "sonner"; import { isApiError } from "../api/types"; import { mapApiErrorMessage } from "../api/errorMessages"; +import { TEST_IDS } from "../lib/testIds"; function errorMessage(error: unknown, fallback: string) { if (isApiError(error)) { @@ -135,7 +136,13 @@ export function WeeklyReportsPage() {
按周归档
默认按周一归档;提交后会同步更新 Portal 与项目摘要。
- setWeekStart(event.target.value)} /> + setWeekStart(event.target.value)} + />
@@ -151,16 +158,16 @@ export function WeeklyReportsPage() { {reportQuery.isError ?
{errorMessage(reportQuery.error, "加载周报失败")}
: null}
- -