diff --git a/.trellis/spec/hyb-oa/approval-workflow.md b/.trellis/spec/hyb-oa/approval-workflow.md index 933f2845..a0d02eb0 100644 --- a/.trellis/spec/hyb-oa/approval-workflow.md +++ b/.trellis/spec/hyb-oa/approval-workflow.md @@ -489,3 +489,74 @@ const label = status === "draft" ? "草稿" : status; {["reviewed", "failed"].includes(batch.status) ? : null} {needsBankReceipt ? : canRegister ? : null} ``` + +## Scenario: Shared Payment Processing Projection + +### 1. Scope / Trigger + +- Trigger: adding or changing payment-stage labels, status projection, receipt/order guidance, reimbursement progress or payment-batch detail progress. +- This is a read-only cross-layer projection. It does not add workflow tasks, alter the payment write state machine or relax receipt/maker-checker gates. + +### 2. Signatures + +- Shared service: `server/src/services/paymentProcessingProgress.js`. +- Builder: `buildPaymentProcessingSemantics({ status, createdAt, reviewedAt, executedAt, cancelledAt, cancelledReason, requiresBankReceipt, receiptCount, paymentOrderExportCount })`. +- Channel helpers: `resolveItemPaymentChannel(batch, item)` and `requiresBankReceipt(batch)`. +- Payment detail: `GET /api/payment-batches/:id` returns `processingProgress.stages[]`; list responses do not return this field. +- Stage states: `completed | current | pending | exception | terminated`. + +### 3. Contracts + +- The shared service is the only runtime source for payment stage keys, labels and `draft|reviewed|executed|failed|cancelled` state projection. Reimbursement and payment-batch routes only assemble their own DTO view, resolve actor names and add permission-gated links. +- Reimbursement keeps its single-item `paymentBatchItem` view; payment detail keeps the whole-batch view. Do not replace these with one database-aware DTO builder, and do not add route-local status branches after calling the shared builder. +- `requiresBankReceipt` is shared with the execute guard and resolves the effective item channel, including the legacy null-channel reimbursement fallback to `bank`. Receipt count alone must never decide whether a receipt is required. +- A reviewed non-bank batch is current at `付款完成` with guidance to register completion. It must not mention uploading a bank receipt or generating a bank order. +- A reviewed bank batch with no active receipt distinguishes no order vs order generated; an active receipt changes guidance to registration. Payment-order count affects copy only when `requiresBankReceipt=true`. +- `failed` uses `exception`. A cancellation before review makes the review stage `exception` and payment stage `terminated`; a cancellation after review completes review and makes payment `exception`. `terminated` is a neutral stopped state, not pending, completed or failed. +- `actorSource=creator|reviewer|executor|canceller` is an internal semantic reference. The shared service never queries employees; each response assembler resolves only a nullable actor name and never exposes actor IDs inside progress. + +### 4. Validation & Error Matrix + +- Unknown/missing status -> created remains completed and downstream stages remain pending; do not guess a current or completed business fact. +- `reviewed + requiresBankReceipt=false` -> current payment guidance is registration only, regardless of receipt/order counts. +- `reviewed + requiresBankReceipt=true + receiptCount=0 + exportCount=0` -> guide offline payment and receipt upload. +- Same with `exportCount>0` -> say the bank order was generated and wait for payment/receipt; do not imply money moved. +- `reviewed + requiresBankReceipt=true + receiptCount>0` -> guide registration completion. +- `cancelled + reviewedAt=null` -> review `exception`, payment `terminated`; `cancelled + reviewedAt!=null` -> review `completed`, payment `exception`. +- Missing actor row or timestamp -> corresponding name/time is null; the detail response still succeeds. + +### 5. Good/Base/Bad Cases + +- Good: both pages import the same semantic builder, while the reimbursement route selects two payment stages and the payment route returns all three. +- Base: a cash batch without a receipt shows `等待财务登记付款完成` and exposes the permitted registration action. +- Bad: a route adds `if (batch.status === "reviewed")` to rewrite stage state/detail, or a client infers receipt requirements from `receiptCount===0`. +- Bad: a cancelled batch shows a later `待处理` node, or a cash batch tells finance to upload a bank receipt. + +### 6. Tests Required + +- Pure table-driven unit tests cover all five statuses, both cancellation positions, bank/non-bank channels, order count, receipt count and actorSource. +- A structural test extracts both route assembler functions and rejects route-local payment status matrices or duplicate `付款复核` / `付款完成` label definitions. +- Route tests assert actor name/time projection, list/detail field separation and absence of snapshot, bank ciphertext and actor IDs inside progress. +- PC/mobile E2E uses synthetic responses for all five states, verifies `terminated`, bank/non-bank copy, one visible primary action per viewport and upload-to-register in-place switching. +- Reimbursement E2E asserts the four-block order and that submit, print, remind, withdraw and delete retain their state/permission, reason/confirmation, request and refresh contracts after action-panel migration. + +### 7. Wrong vs Correct + +#### Wrong + +```js +if (batch.status === "reviewed" && receiptCount === 0) { + paymentStage.detail = "请上传银行回单"; +} +``` + +#### Correct + +```js +const semantics = buildPaymentProcessingSemantics({ + status: batch.status, + requiresBankReceipt: requiresBankReceipt(batch), + receiptCount: batch._count?.receipt_attachments ?? 0, + paymentOrderExportCount: batch.payment_order_export_count ?? 0, +}); +``` diff --git a/.trellis/tasks/08-03-detail-page-structure-progress-actions/check.jsonl b/.trellis/tasks/08-03-detail-page-structure-progress-actions/check.jsonl new file mode 100644 index 00000000..4152e4fe --- /dev/null +++ b/.trellis/tasks/08-03-detail-page-structure-progress-actions/check.jsonl @@ -0,0 +1,3 @@ +{"file":".trellis/spec/hyb-oa/approval-workflow.md","reason":"只读复核五状态、取消终止态和 maker-checker 语义未漂移"} +{"file":".trellis/spec/hyb-oa/security-privacy.md","reason":"复核新增付款进度 DTO 不扩大银行敏感信息披露"} +{"file":".trellis/spec/hyb-oa/project-governance.md","reason":"状态机相关变更的只读 reviewer 与交付门禁"} diff --git a/.trellis/tasks/08-03-detail-page-structure-progress-actions/design.md b/.trellis/tasks/08-03-detail-page-structure-progress-actions/design.md new file mode 100644 index 00000000..3019bee6 --- /dev/null +++ b/.trellis/tasks/08-03-detail-page-structure-progress-actions/design.md @@ -0,0 +1,109 @@ +# 详情页结构统一与付款处理时间线设计 + +## 1. 设计边界 + +本任务不合并报销 workflow 与付款批次状态机,只建立共享的只读付款阶段投影,并在两个业务视角中组装各自 DTO。数据库、付款写接口、权限和审计均保持不变。 + +前端复用现有详情工作区、时间线、固定底栏、确认弹窗和按钮组件。项目声明的 `frontend-design` skill 当前不可用,本轮不发明新的视觉体系;规划阶段已使用可用的 `ui-ux-pro-max` 检查 sticky 遮挡、异步反馈、派生状态和移动端安全区原则,最终以仓库既有成熟详情模式为实现依据。 + +## 2. 服务端共享语义层 + +新增一个无数据库依赖的付款进度服务模块,例如 `server/src/services/paymentProcessingProgress.js`。模块导出: + +- 唯一的阶段定义:`payment_batch_created / 批次创建`、`payment_batch_review / 付款复核`、`payment_and_receipt / 付款完成`。 +- 唯一状态集合:`completed | current | pending | exception | terminated`。 +- `buildPaymentProcessingSemantics(input)`:输入批次状态、复核/执行/取消时间、取消原因、`requiresBankReceipt`、回单数和制单次数,返回三个阶段的 state、result、detail、comment、actorSource 与 occurredAt。 + +`actorSource` 只使用 `creator/reviewer/executor/canceller` 这类内部语义引用,不在共享层查询姓名,也不产生页面链接。状态矩阵: + +| 批次事实 | 批次创建 | 付款复核 | 付款完成 | +| --- | --- | --- | --- | +| draft | completed | current | pending | +| reviewed,需要回单、无回单、未制单 | completed | completed | current:等待线下付款并上传回单 | +| reviewed,需要回单、无回单、已制单 | completed | completed | current:已生成制单,等待付款并上传回单 | +| reviewed,需要回单、已有回单 | completed | completed | current:等待登记付款完成 | +| reviewed,不需要回单 | completed | completed | current:等待登记付款完成 | +| executed | completed | completed | completed | +| failed | completed | completed | exception | +| cancelled,`reviewedAt=null` | completed | exception(取消) | terminated | +| cancelled,`reviewedAt!=null` | completed | completed | exception(取消) | + +取消节点使用取消人、取消时间与原因;失败节点使用最后执行人和执行时间。语义函数不猜测缺失人员,时间戳缺失时保持 null。 + +`requiresBankReceipt` 必须复用付款执行门禁的渠道判断:只要仍待处理/失败的批次项中存在有效付款渠道为 `bank` 的项目即为 true。路由不得为时间线再发明一套渠道判断;最小实现是提取并共享现有 `requiresBankReceipt` 计算函数。只有该值为 true 时,制单次数才参与阶段文案。 + +## 3. 两个视图组装层 + +### 3.1 报销详情 + +`buildProcessingProgress` 继续组装提交与真实 workflow task。付款部分调用共享语义函数并只选择 `payment_batch_review`、`payment_and_receipt`: + +- 通过已有 `paymentActorNames` 把 actorSource 对应 ID 解析为姓名。 +- 按现有权限决定 `targetHref`。 +- 无付款批次时保留“待财务安排付款”的报销专属投影。 +- 历史 `paid` 且无批次时继续用报销 `paid_by/paid_at` 完成最后阶段。 + +报销组装层不得重新按 batch status 写 switch/if;批次存在时的 state、result 和基础 detail 一律来自共享语义。 + +### 3.2 付款批次详情 + +付款批次详情 DTO 新增 `processingProgress.stages[]`,使用共享语义的三个阶段: + +- `mapBatch` 列表响应保持轻量,不额外查姓名。 +- `mapBatchForResponse` 仅为详情及写操作返回结果收集 created/reviewed/executed/cancelled actor ID,以一次 `employee.findMany` 解析姓名,然后组装 progress。 +- API 类型拆成列表基础类型与带必需 `processingProgress` 的详情类型,避免把详情字段伪装成所有列表项都存在。 +- progress 只含姓名和时间,不增加员工号、角色、账号、快照或相邻批次信息。 + +服务端结构测试直接导入共享阶段定义/构建器,并验证两个路由没有各自状态矩阵,防止未来再次分叉。 + +## 4. 共享时间线组件 + +将 `ReimbursementProcessingProgress` 改名并移动到共享 patterns/common 目录,例如 `ProcessingProgress.tsx`。保持单个 `stages.map`: + +- 组件自带通用 `ProcessingStage` / `ProcessingProgress` 类型,报销与付款 API 类型复用或结构兼容。 +- props 参数化 `ariaLabel`、`testId`、`mobile` 与目标链接文案;报销仍显示“查看所在付款批次”,付款批次本身不生成自指链接。 +- 新增 terminated 的中性图标、灰色连接/节点与“已终止”,不使用红色异常或绿色完成视觉。 +- current/exception/comment 继续展开 detail;pending/terminated 不显示会误导为下一步的说明。 + +## 5. 页面结构 + +### 5.1 报销 PC + +- hero 仅保留标题和状态;现有 primary/secondary business actions 迁入右侧 `BusinessDetailActionPanel`。 +- `BusinessDetailMain` 顺序为概况、处理进度、明细、材料。 +- `BusinessDetailAside` 承载一个操作面板;提交、打印、催办、撤回、删除等继续使用现有权限、mutation 和确认逻辑。 +- 删除 `ReimbursementActions` 的独立卡片实现及三个进度锚点;不复制动作。 + +### 5.2 报销移动端 + +只移动现有处理进度 section 到概况后;明细、材料随后,底部操作 section 与现有固定提交栏保持末尾/safe-area 合同。 + +### 5.3 付款批次响应式详情 + +- `BusinessDetailMain` 为概况、处理进度、明细、回单;旧 `payment-batch-current-processing` 事实卡删除。 +- `BusinessDetailAside` 在 `lg+` 显示 `BusinessDetailActionPanel`。 +- 同一 `paymentActionModel` 从权限、批次状态、制单次数、回单查询和 mutation 状态派生唯一 primary、secondary、more action;PC 面板和移动端只负责渲染。 +- `(`/api/payment-batches/${batchId}`); + return apiRequestData<{ item: PaymentBatchDetail }>(`/api/payment-batches/${batchId}`); } export async function fetchPaymentBatchCandidates(query: { q?: string; page?: number; pageSize?: number } = {}) { diff --git a/client/src/api/reimbursements.ts b/client/src/api/reimbursements.ts index c3119dad..32591a56 100644 --- a/client/src/api/reimbursements.ts +++ b/client/src/api/reimbursements.ts @@ -1,6 +1,7 @@ import { apiRequestBlob, apiRequestData } from "./http"; import type { ExpenseStandardCheck, PrecheckResponse, RiskFlag } from "./feeControl"; import { buildAttachmentFileUrl, type AttachmentGovernanceFields } from "../lib/attachmentGovernance"; +import type { ProcessingProgress, ProcessingStage } from "../types/processingProgress"; export type ReimbursementStatus = | "draft" @@ -112,21 +113,8 @@ export type ReimbursementWorkflowProgress = { }>; }; -export type ReimbursementProcessingStage = { - key: string; - label: string; - state: "completed" | "current" | "pending" | "exception"; - actorName: string | null; - occurredAt: string | null; - result: string | null; - comment: string | null; - detail: string | null; - targetHref: string | null; -}; - -export type ReimbursementProcessingProgress = { - stages: ReimbursementProcessingStage[]; -}; +export type ReimbursementProcessingStage = ProcessingStage; +export type ReimbursementProcessingProgress = ProcessingProgress; export type ReimbursementApprovalHistoryItem = { id: string; diff --git a/client/src/components/patterns/BusinessDetailTemplate.tsx b/client/src/components/patterns/BusinessDetailTemplate.tsx index 9be9a6c4..f3770d17 100644 --- a/client/src/components/patterns/BusinessDetailTemplate.tsx +++ b/client/src/components/patterns/BusinessDetailTemplate.tsx @@ -98,7 +98,7 @@ export function BusinessDetailActionPanel({ const hasContentBeforeSummary = Boolean(primaryAction || secondaryActions || moreActions || hasChildren); return ( - +
{primaryAction ?
{primaryAction}
: null} {secondaryActions ?
{secondaryActions}
: null} diff --git a/client/src/components/reimbursements/ReimbursementProcessingProgress.tsx b/client/src/components/patterns/ProcessingProgress.tsx similarity index 80% rename from client/src/components/reimbursements/ReimbursementProcessingProgress.tsx rename to client/src/components/patterns/ProcessingProgress.tsx index b3d113f5..f0da4992 100644 --- a/client/src/components/reimbursements/ReimbursementProcessingProgress.tsx +++ b/client/src/components/patterns/ProcessingProgress.tsx @@ -1,12 +1,15 @@ -import { AlertCircle, Check, Circle, Clock, ExternalLink } from "lucide-react"; +import { AlertCircle, Ban, Check, Circle, Clock, ExternalLink } from "lucide-react"; import { Link } from "react-router-dom"; -import type { ReimbursementProcessingProgress as ProcessingProgress } from "../../api/reimbursements"; +import type { ProcessingProgress as ProcessingProgressValue } from "../../types/processingProgress"; import { formatDisplayDateTime } from "../../lib/displayFormat"; import { cn } from "../../lib/utils"; type Props = { - progress: ProcessingProgress; + progress: ProcessingProgressValue; + ariaLabel: string; + testId: string; mobile?: boolean; + targetLinkLabel?: string; }; const STATE_TEXT = { @@ -14,15 +17,18 @@ const STATE_TEXT = { current: "处理中", pending: "待处理", exception: "需处理", + terminated: "已终止", } as const; -export function ReimbursementProcessingProgress({ progress, mobile = false }: Props) { +export function ProcessingProgress({ + progress, + ariaLabel, + testId, + mobile = false, + targetLinkLabel = "查看详情", +}: Props) { return ( -
    +
      {progress.stages.map((stage, index) => { const expanded = stage.state === "current" || stage.state === "exception" || Boolean(stage.comment); const Icon = stage.state === "completed" @@ -31,7 +37,9 @@ export function ReimbursementProcessingProgress({ progress, mobile = false }: Pr ? Clock : stage.state === "exception" ? AlertCircle - : Circle; + : stage.state === "terminated" + ? Ban + : Circle; const facts = [stage.actorName, stage.occurredAt ? formatDisplayDateTime(stage.occurredAt) : null, stage.result] .filter(Boolean); @@ -47,6 +55,7 @@ export function ReimbursementProcessingProgress({ progress, mobile = false }: Pr stage.state === "current" && "border-blue-300 bg-blue-50 text-blue-700", stage.state === "exception" && "border-red-300 bg-red-50 text-red-700", stage.state === "pending" && "border-zinc-300 text-zinc-400", + stage.state === "terminated" && "border-zinc-300 bg-zinc-100 text-zinc-500", )} aria-hidden="true" > @@ -69,7 +78,7 @@ export function ReimbursementProcessingProgress({ progress, mobile = false }: Pr className="mt-2 inline-flex min-h-11 items-center gap-1 text-sm font-medium text-primary hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-600 focus-visible:ring-offset-2" to={stage.targetHref} > - 查看所在付款批次 + {targetLinkLabel}
) : selectedBatch ? ( -
+
+ +
@@ -555,89 +670,7 @@ export function PaymentBatchesPage() { ) : null}
-
- {canReviewSelectedBatch ? ( - - ) : null} - {selectedBatch.status === "reviewed" && selectedBatch.canExportPaymentOrder && canExportPaymentOrder && selectedBatch.paymentOrderExportCount === 0 ? ( - - ) : null} - {needsBankReceipt ? ( - - ) : null} - {canRegisterPaymentComplete ? ( - - ) : null} - {canManageBatch ? ( - - ) : null} - {selectedBatch.status === "reviewed" && selectedBatch.canExportPaymentOrder && canExportPaymentOrder && selectedBatch.paymentOrderExportCount > 0 ? ( - - ) : null} -
+
状态
{paymentBatchStatusLabel(selectedBatch.status)}
@@ -661,59 +694,13 @@ export function PaymentBatchesPage() { ) : null}
-
-

当前处理

-
-
-
当前责任
-
- {selectedBatch.status === "draft" - ? "负责人(非制单人)" - : ["reviewed", "failed"].includes(selectedBatch.status) - ? "财务" - : selectedBatch.status === "executed" - ? "已完成" - : "已结束"} -
-
-
-
下一步
-
- {selectedBatch.status === "draft" - ? "复核通过" - : selectedBatch.status === "reviewed" && requiresBankReceipt && !receiptAttachmentsQuery.isSuccess - ? "核对银行回单状态" - : selectedBatch.status === "reviewed" && needsBankReceipt - ? selectedBatch.paymentOrderExportCount > 0 - ? "制单已生成,完成付款并上传银行回单" - : "线下付款并上传银行回单" - : selectedBatch.status === "reviewed" - ? "登记付款完成" - : selectedBatch.status === "failed" - ? "核对失败原因并重试" - : "-"} -
-
-
-
- {selectedBatch.status === "draft" - ? "已提交付款复核,等待负责人处理;制单人不能复核自己的批次。" - : selectedBatch.status === "reviewed" && requiresBankReceipt && !receiptAttachmentsQuery.isSuccess - ? "付款复核已通过,正在核对银行回单状态。" - : selectedBatch.status === "reviewed" && needsBankReceipt - ? selectedBatch.paymentOrderExportCount > 0 - ? "已生成银行制单,等待付款完成并上传回单。" - : "付款复核已通过。等待财务线下付款并上传回单。" - : selectedBatch.status === "reviewed" - ? "付款复核已通过。请财务核对付款结果并登记付款完成。" - : selectedBatch.status === "executed" - ? "付款已登记完成。" - : selectedBatch.status === "failed" - ? "付款登记失败,请财务核对失败原因后重试。" - : "该付款批次已取消。"} - {selectedBatch.highRisk ? ` 批次金额达到 ${formatMoney(selectedBatch.reviewRequirement.thresholdAmount)},处理前需再次核对付款对象、金额和回单,但不增加审批层级。` : ""} - {selectedBatch.cancelledReason ? ` 取消原因:${selectedBatch.cancelledReason}` : ""} -
+
+

处理进度

+
@@ -775,9 +762,11 @@ export function PaymentBatchesPage() {

银行回单

- {selectedBatch.status === "draft" - ? "付款复核通过后,由财务在线下完成付款并上传银行回单。" - : "先在线下银行完成付款,再上传回单;系统不会连接银行自动转账。"} + {!requiresBankReceipt + ? "当前付款渠道不要求银行回单,核对付款结果后可直接登记完成。" + : selectedBatch.status === "draft" + ? "付款复核通过后,由财务在线下完成付款并上传银行回单。" + : "先在线下银行完成付款,再上传回单;系统不会连接银行自动转账。"}

{canUploadReceipt ? ( @@ -866,14 +855,70 @@ export function PaymentBatchesPage() {
))} - {receiptAttachmentsQuery.isSuccess && !(receiptAttachmentsQuery.data?.items ?? []).length ? ( + {requiresBankReceipt && receiptAttachmentsQuery.isSuccess && !(receiptAttachmentsQuery.data?.items ?? []).length ? (
暂无银行回单;银行转账必须先上传回单,才能登记付款完成。
) : null}
- + + + + } + primaryAction={paymentActionModel.primary ? renderPaymentActionButton(paymentActionModel.primary) : undefined} + moreActions={paymentActionModel.more.length ? ( +
+ 更多 +
+ {paymentActionModel.more.map((kind) => + renderPaymentActionButton(kind, kind === "cancel" ? "destructive" : "secondary"), + )} +
+
+ ) : undefined} + summary={( + <> +

批次:{selectedBatch.batchNo}

+

金额:{formatMoney(selectedBatch.totalAmount)}

+ + )} + testId="payment-batch-action-panel" + /> +
+ + + {paymentActionModel.primary || paymentActionModel.more.length ? ( +
+
+ {paymentActionModel.primary ? renderPaymentActionButton(paymentActionModel.primary) : } + {paymentActionModel.more.length ? ( + + + + + + {paymentActionModel.more.map((kind) => ( + activatePaymentAction(kind)} + > + {paymentActionLabel(kind)} + + ))} + + + ) : null} +
+
+ ) : null} + ) : (
未找到付款批次
) diff --git a/client/src/pages/ReimbursementDetailPage.tsx b/client/src/pages/ReimbursementDetailPage.tsx index a1adc44e..3bd6daec 100644 --- a/client/src/pages/ReimbursementDetailPage.tsx +++ b/client/src/pages/ReimbursementDetailPage.tsx @@ -47,7 +47,7 @@ import { AttachmentDeleteConfirmDialog } from "../components/common/AttachmentDe import { useAttachmentDownloadReasonDialog } from "../components/common/useAttachmentDownloadReasonDialog"; import { RiskFlagsCard } from "../components/common/RiskFlagsCard"; import { ReimbursementTravelSummary } from "../components/reimbursements/ReimbursementTravelSummary"; -import { ReimbursementProcessingProgress } from "../components/reimbursements/ReimbursementProcessingProgress"; +import { ProcessingProgress } from "../components/patterns/ProcessingProgress"; import { StatusBadge } from "../components/common/StatusBadge"; import { Button } from "../components/ui/button"; import { @@ -60,6 +60,12 @@ import { import { Input } from "../components/ui/input"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "../components/ui/table"; import { EntityOverviewHero } from "../components/patterns/EntityOverviewHero"; +import { + BusinessDetailActionPanel, + BusinessDetailAside, + BusinessDetailMain, + BusinessDetailWorkspace, +} from "../components/patterns/BusinessDetailTemplate"; import { PageScaffold } from "../components/patterns/PageScaffold"; import { SectionPanel } from "../components/patterns/SectionPanel"; import { @@ -1028,30 +1034,6 @@ export function ReimbursementDetailPage() { ); } - if (detail.status === "reviewing") { - return ( - - ); - } - - if (detail.status === "approved") { - return ( - - ); - } - - if (detail.status === "paid") { - return ( - - ); - } - return null; })(); @@ -1105,16 +1087,10 @@ export function ReimbursementDetailPage() { )} - actions={( -
- {primaryAction} - {secondaryActions} -
- )} /> -
-
+ +
@@ -1227,35 +1203,17 @@ export function ReimbursementDetailPage() { ) : null}
- {showSecondaryDetail ? ( -
-
-
-

更多

-
- {detail.status !== "paid" ? ( - - ) : null} - {canDeleteReimbursement ? ( - - ) : null} -
-
-
-
- ) : null} + + + +
@@ -1574,27 +1532,49 @@ export function ReimbursementDetailPage() {
- -
- -
-
-
+ -
- 0} - submitHint="" - submitNotice="" - isPending={mutation.isPending} - onSubmit={() => void handleSubmitAction()} - onWithdraw={(comment) => mutation.mutate({ action: "withdraw", comment })} - /> -
-
+ + } + primaryAction={primaryAction} + secondaryActions={secondaryActions} + moreActions={showSecondaryDetail ? ( +
+ {detail.status !== "paid" ? ( + + ) : null} + {canDeleteReimbursement ? ( + + ) : null} +
+ ) : null} + testId="reimbursement-action-panel" + > + 0} + submitHint="" + submitNotice="" + isPending={mutation.isPending} + onSubmit={() => void handleSubmitAction()} + onWithdraw={(comment) => mutation.mutate({ action: "withdraw", comment })} + /> +
+
+