收敛报销明细与材料交互(07-27-yang-jun-reimbursement-readiness)
ci-e2e / e2e (push) Canceled after 0s
ci-minimal / check-minimal (push) Canceled after 0s

这个提交包含在:
2026-07-31 22:01:25 +08:00
父节点 665dcabdc7
当前提交 0e44aa72f3
共修改 7 个文件,包含 407 行新增和 238 行删除
@@ -576,6 +576,98 @@ reply.header(
);
```
## Scenario: Reimbursement Material Presentation And Item Linking
### 1. Scope / Trigger
- Trigger: changing PC or mobile reimbursement-detail presentation for invoice
originals, supporting attachments or their relationship to expense items.
- Purpose: let finance reconcile each item with its invoice without exposing a
separate technical “open file” button for every material.
### 2. Signatures
- Existing detail payload: `detail.items[]`, `detail.invoices[]` with
`invoice.linkedItemId: string | null`, and `detail.attachments[]`.
- Existing preview actions: `openInvoiceMutation` and
`handleOpenAttachment`; WeCom continues through `previewFile` and PC through
its existing browser-safe path.
- Stable UI selectors: `reimbursement-materials` and
`mobile-reimbursement-materials`; existing invoice/attachment child test IDs
remain available for focused regressions.
### 3. Contracts
- Present invoice originals and supporting attachments under one user-facing
`报销材料` section with separate type labels and counts. Do not merge their
database types, API fields, authorization, audit or lifecycle rules.
- Within each expense item, render only invoices whose
`linkedItemId === item.id`. The linked invoice summary is itself a semantic
preview button and shows an invoice identifier, seller and amount.
- Keep every invoice, including unlinked or stale-linked invoices, in the
material section. Never infer a link from amount, order or list position.
- The material information row is the preview target; do not append a repeated
visible `查看原件` button to each file. Draft-only add/remove/upload/delete
actions remain separate buttons so preview cannot mutate data.
- Do not introduce a file-browser route, carousel, bulk download or aggregate
backend endpoint for the small-team detail page unless later real use proves
that the inline material list is insufficient.
- Clickable rows have a descriptive `aria-label`, visible focus, disabled state
during open, a chevron affordance and a minimum 44px mobile touch target.
### 4. Validation & Error Matrix
- Matching `linkedItemId` -> show the invoice inside that item and in the
material section.
- `linkedItemId=null` or target item absent -> show only in the material
section; do not attach it to the first or amount-matching item.
- No invoices and no attachments -> one `暂无报销材料` empty state.
- Preview pending -> disable repeated opens; existing preview errors and
download-reason behavior remain authoritative.
- Read-only approver -> preview rows visible, mutation actions absent.
### 5. Good/Base/Bad Cases
- Good: a hotel item shows its linked hotel invoice inline, while an unlinked
payment screenshot remains under `报销材料` and both originals are previewable.
- Base: one item, one linked invoice and no attachment renders one inline
invoice summary plus one typed material row.
- Bad: flatten invoices away from items, guess links by equal amount, merge all
file records into one backend type, or add a `查看原件` button beside every
row.
### 6. Tests Required
- Focused PC and WeCom Playwright fixtures contain at least one linked and one
unlinked invoice plus one supporting attachment.
- Assert the linked invoice appears only in the matching item; the unlinked
invoice appears in materials but not in any item.
- Assert material counts, absence of exact visible `查看原件` buttons, PC open
URL behavior, WeCom `previewFile` name/size/URL, 44px touch target and mobile
portrait/landscape overflow.
- Preserve editable add/remove/upload/delete regression coverage, then run
frontend typecheck/build and Trellis governance gates.
### 7. Wrong vs Correct
#### Wrong
```tsx
detail.invoices.map((invoice) => <Button>查看原件</Button>);
```
#### Correct
```tsx
detail.invoices
.filter((invoice) => invoice.linkedItemId === item.id)
.map((invoice) => (
<button aria-label={`查看关联发票:${formatInvoiceDisplayTitle(invoice)}`}>
{formatInvoiceDisplayTitle(invoice)}
</button>
));
```
## Scenario: Payment Batch Receipt Attention Count
### 1. Scope / Trigger