RI6 完善报销票据体验与状态文案
这个提交包含在:
@@ -53,7 +53,12 @@ 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 { formatInvoiceOcrErrorMessage } from "../lib/invoiceOcr";
|
||||
import {
|
||||
formatInvoiceDisplayTitle,
|
||||
resolveInvoiceNextStepText,
|
||||
resolveInvoiceStatusLabel,
|
||||
resolveInvoiceUserMessage,
|
||||
} from "../lib/invoiceStatus";
|
||||
import { useInvoiceOcrTracker } from "../hooks/useInvoiceOcrTracker";
|
||||
import { resolveReimbursementDisplayCode } from "../lib/reimbursementDisplay";
|
||||
import { TEST_IDS } from "../lib/testIds";
|
||||
@@ -77,14 +82,6 @@ const REIMBURSEMENT_STATUS_LABEL: Record<ReimbursementStatus, string> = {
|
||||
cancelled: "已取消",
|
||||
};
|
||||
|
||||
const INVOICE_STATUS_LABEL: Record<string, string> = {
|
||||
pending_ocr: "识别中",
|
||||
ocr_failed: "识别失败",
|
||||
available: "可添加",
|
||||
bound: "已添加",
|
||||
archived: "已归档",
|
||||
};
|
||||
|
||||
function formatDateTime(value: string | null) {
|
||||
if (!value) return "-";
|
||||
return value.slice(0, 19).replace("T", " ");
|
||||
@@ -106,10 +103,6 @@ function reimbursementStatusLabel(status: ReimbursementStatus) {
|
||||
return REIMBURSEMENT_STATUS_LABEL[status] ?? "未知状态";
|
||||
}
|
||||
|
||||
function invoiceStatusLabel(status: string) {
|
||||
return INVOICE_STATUS_LABEL[status] ?? "未知状态";
|
||||
}
|
||||
|
||||
function workflowActionLabel(action: string) {
|
||||
const labels: Record<string, string> = {
|
||||
start: "提交",
|
||||
@@ -547,7 +540,10 @@ export function ReimbursementDetailPage() {
|
||||
}
|
||||
|
||||
const summary = failedResults
|
||||
.map((item) => `${item.invoiceId.slice(0, 8)}: ${item.message ?? "添加失败"}`)
|
||||
.map((item) => {
|
||||
const invoice = (bindableInvoicesQuery.data?.items ?? []).find((candidate) => candidate.id === item.invoiceId);
|
||||
return `${formatInvoiceDisplayTitle(invoice)}:${item.message ?? "添加失败"}`;
|
||||
})
|
||||
.join(";");
|
||||
setInvoiceActionError(summary);
|
||||
if (succeededIds.length) {
|
||||
@@ -575,7 +571,7 @@ export function ReimbursementDetailPage() {
|
||||
|
||||
const handleTrackedInvoicesUnavailable = useCallback((items: Array<{ id: string; originalName: string | null; message: string; availabilityMessage?: string | null; ocrLastError?: string | null }>) => {
|
||||
const message = items
|
||||
.map((item) => `${item.originalName ?? "未命名发票"}: ${item.availabilityMessage ?? item.message ?? formatInvoiceOcrErrorMessage(item.ocrLastError)}`)
|
||||
.map((item) => `${formatInvoiceDisplayTitle(item)}:${resolveInvoiceUserMessage(item)}`)
|
||||
.join(";");
|
||||
setInvoiceActionError(message);
|
||||
toast.error("部分发票需要处理");
|
||||
@@ -633,7 +629,7 @@ export function ReimbursementDetailPage() {
|
||||
if (result.uploadAction === "blocked" || !result.invoicePoolId) {
|
||||
uploadFailures.push({
|
||||
file,
|
||||
message: result.unavailableMessage ?? "该发票不可添加",
|
||||
message: resolveInvoiceUserMessage(result),
|
||||
});
|
||||
} else if (result.status === "available") {
|
||||
availableIds.push(result.invoicePoolId);
|
||||
@@ -642,7 +638,7 @@ export function ReimbursementDetailPage() {
|
||||
} else {
|
||||
uploadFailures.push({
|
||||
file,
|
||||
message: result.unavailableMessage ?? "该发票不可添加",
|
||||
message: resolveInvoiceUserMessage(result),
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -660,7 +656,7 @@ export function ReimbursementDetailPage() {
|
||||
await bindInvoice(invoiceId, { reimbursementId });
|
||||
boundCount += 1;
|
||||
} catch (error) {
|
||||
bindFailures.push(`${invoiceId.slice(0, 8)}: ${buildActionErrorMessage(error, "添加发票失败")}`);
|
||||
bindFailures.push(`新上传发票:${buildActionErrorMessage(error, "添加发票失败")}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1404,45 +1400,53 @@ export function ReimbursementDetailPage() {
|
||||
|
||||
{(detail.invoices ?? []).length ? (
|
||||
<div className="space-y-3">
|
||||
{detail.invoices.map((item) => (
|
||||
<div
|
||||
className="rounded-lg border border-zinc-200 px-4 py-3"
|
||||
key={item.id}
|
||||
>
|
||||
<div className="flex flex-col gap-3 md:flex-row md:items-start md:justify-between">
|
||||
<div className="space-y-1">
|
||||
<div className="font-medium text-zinc-900">
|
||||
发票 {item.invoiceCode || "-"}-{item.invoiceNumber || "-"}
|
||||
{detail.invoices.map((item) => {
|
||||
const invoiceHint = resolveInvoiceNextStepText(item);
|
||||
return (
|
||||
<div
|
||||
className="rounded-lg border border-zinc-200 px-4 py-3"
|
||||
key={item.id}
|
||||
>
|
||||
<div className="flex flex-col gap-3 md:flex-row md:items-start md:justify-between">
|
||||
<div className="space-y-1">
|
||||
<div className="font-medium text-zinc-900">
|
||||
{formatInvoiceDisplayTitle(item)}
|
||||
</div>
|
||||
<div className="text-zinc-600">
|
||||
{item.sellerName || "未识别销售方"} · {item.invoiceDate || "未识别日期"} · 金额 ¥{item.totalAmount ?? "-"}
|
||||
</div>
|
||||
<div className="text-xs text-zinc-500">
|
||||
原始文件:{item.originalName || "未记录原始文件"}
|
||||
</div>
|
||||
<div className="text-xs text-zinc-500">
|
||||
明细:{resolveLinkedItemLabel(item.linkedItemId)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-zinc-600">
|
||||
{item.sellerName || "未识别卖方"} · {item.invoiceDate || "未识别日期"} · 金额 ¥{item.totalAmount ?? "-"}
|
||||
<div className="flex flex-col items-start gap-2 md:items-end">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="rounded-full bg-zinc-100 px-2 py-1 text-xs text-zinc-600">
|
||||
{resolveInvoiceStatusLabel(item.status)}
|
||||
</span>
|
||||
{canUnbindInvoices ? (
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
disabled={unbindInvoiceMutation.isPending}
|
||||
onClick={() => unbindInvoiceMutation.mutate(item.id)}
|
||||
>
|
||||
移除
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
{invoiceHint ? (
|
||||
<div className="max-w-[260px] text-xs text-zinc-500 md:text-right">{invoiceHint}</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="text-xs text-zinc-500">
|
||||
原始文件:{item.originalName || item.id}
|
||||
</div>
|
||||
<div className="text-xs text-zinc-500">
|
||||
明细:{resolveLinkedItemLabel(item.linkedItemId)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="rounded-full bg-zinc-100 px-2 py-1 text-xs text-zinc-600">
|
||||
{invoiceStatusLabel(item.status)}
|
||||
</span>
|
||||
{canUnbindInvoices ? (
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
disabled={unbindInvoiceMutation.isPending}
|
||||
onClick={() => unbindInvoiceMutation.mutate(item.id)}
|
||||
>
|
||||
移除
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-sm text-zinc-500">暂无发票</p>
|
||||
@@ -1633,7 +1637,7 @@ export function ReimbursementDetailPage() {
|
||||
>
|
||||
<DialogHeader>
|
||||
<DialogTitle>添加发票</DialogTitle>
|
||||
<DialogDescription>选择可用发票,或上传新发票。</DialogDescription>
|
||||
<DialogDescription>选择可添加发票,或上传新发票。</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4">
|
||||
@@ -1669,6 +1673,7 @@ export function ReimbursementDetailPage() {
|
||||
) : (bindableInvoicesQuery.data?.items ?? []).length ? (
|
||||
bindableInvoicesQuery.data?.items.map((item) => {
|
||||
const isSelected = Object.prototype.hasOwnProperty.call(selectedInvoices, item.id);
|
||||
const invoiceHint = resolveInvoiceNextStepText({ ...item, status: "available" });
|
||||
return (
|
||||
<label
|
||||
key={item.id}
|
||||
@@ -1683,20 +1688,18 @@ export function ReimbursementDetailPage() {
|
||||
onChange={(event) => toggleInvoiceSelection(item.id, event.target.checked)}
|
||||
/>
|
||||
<div className="space-y-1">
|
||||
<div className="font-medium text-zinc-900">
|
||||
发票 {item.invoiceCode || "-"}-{item.invoiceNumber || "-"}
|
||||
</div>
|
||||
<div className="font-medium text-zinc-900">{formatInvoiceDisplayTitle(item)}</div>
|
||||
<div className="text-zinc-600">
|
||||
{item.sellerName || "未识别卖方"} · {item.invoiceDate || "未识别日期"} · 金额 ¥{item.totalAmount ?? "-"}
|
||||
{item.sellerName || "未识别销售方"} · {item.invoiceDate || "未识别日期"} · 金额 ¥{item.totalAmount ?? "-"}
|
||||
</div>
|
||||
<div className="text-xs text-zinc-500">
|
||||
原始文件:{item.originalName || item.id} · 上传时间 {formatDateTime(item.createdAt)}
|
||||
原始文件:{item.originalName || "未记录原始文件"} · 上传时间 {formatDateTime(item.createdAt)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="min-w-[220px] rounded-md bg-primary-50 px-3 py-2 text-xs text-primary-700 md:max-w-[240px]">
|
||||
添加后自动生成或匹配报销明细
|
||||
{invoiceHint} 添加后自动生成或匹配报销明细。
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
@@ -1881,7 +1884,7 @@ export function ReimbursementDetailPage() {
|
||||
open={confirmDeleteReimbursement}
|
||||
onOpenChange={setConfirmDeleteReimbursement}
|
||||
title={`确认删除 ${displayCode}?`}
|
||||
description="删除后无法恢复,已添加发票会回到可用状态。"
|
||||
description="删除后无法恢复,已添加发票会回到可添加状态。"
|
||||
variant="destructive"
|
||||
confirmLabel="删除草稿"
|
||||
onConfirm={() => { deleteReimbursementMutation.mutate(); }}
|
||||
|
||||
在新工单中引用
屏蔽一个用户