feat: 补齐报销付款待办交接(09-11-reimbursement-payment-handoffs)
ci-e2e / e2e (push) Canceled after 0s
ci-minimal / check-minimal (push) Canceled after 0s

这个提交包含在:
2026-09-11 09:43:37 +08:00
父节点 f9aee4c932
当前提交 fedcaed1a1
共修改 15 个文件,包含 486 行新增和 15 行删除
+13
查看文件
@@ -79,6 +79,19 @@ export type PortalHomePayload = {
canViewTeam: boolean;
isAdminOnly: boolean;
};
businessHandoffs?: {
total: number;
items: Array<{
key: string;
kind: "reimbursement_edit" | "reimbursement_batch" | "payment_review" | "payment_execute";
reference: string | null;
title: string;
nextStep: string;
ownerLabel: string;
href: string;
mobileHref: string;
}>;
};
financeSummary?: {
pendingBatchCount: number;
failedItemCount: number;
+23 -4
查看文件
@@ -131,6 +131,9 @@ export function MobilePortalPage() {
enabled: canUseFinance,
queryKey: ["mobile", "portal", "home"],
queryFn: () => fetchPortalHome(),
staleTime: 0,
refetchOnMount: true,
refetchOnWindowFocus: true,
});
useEffect(() => {
@@ -167,13 +170,14 @@ export function MobilePortalPage() {
.filter((item) => !(canAccessApprovals && item.type.startsWith("workflow.task_")))
.slice(0, 3);
const financeSummary = portalHomeQuery.data?.financeSummary ?? null;
const businessHandoffs = portalHomeQuery.data?.businessHandoffs;
const paymentBatchEnabled = Boolean(financeSummary?.paymentBatchEnabled);
const financePendingCount = financeSummary?.pendingBatchCount ?? 0;
const financeExceptionCount = (financeSummary?.failedItemCount ?? 0) + (financeSummary?.riskCount ?? 0);
const financeExceptionCount = (businessHandoffs ? 0 : financeSummary?.failedItemCount ?? 0) + (financeSummary?.riskCount ?? 0);
const shouldShowFinanceCard = canUseFinance &&
!portalHomeQuery.isLoading &&
!portalHomeQuery.isError &&
(paymentBatchEnabled || financePendingCount > 0 || financeExceptionCount > 0);
(businessHandoffs ? financeExceptionCount > 0 : paymentBatchEnabled || financePendingCount > 0 || financeExceptionCount > 0);
const financeCardBadge = paymentBatchEnabled ? "复核与付款" : financePendingCount > 0 ? "待付款" : "异常与复核";
async function openHomeNotification(item: NotificationItem) {
@@ -298,6 +302,21 @@ export function MobilePortalPage() {
</section>
) : null}
{portalHomeQuery.isError ? <section className="mobile-card space-y-3" role="alert"><p>业务待办加载失败,请重试。</p><Button onClick={() => void portalHomeQuery.refetch()} type="button" variant="secondary">重试加载待办</Button></section> : null}
{!portalHomeQuery.isError && businessHandoffs && businessHandoffs.total > 0 ? (
<section className="mobile-card mobile-page-stack" data-testid="mobile-business-handoffs">
<div className="mobile-section-header"><h2 className="mobile-section-title">业务待办</h2><span>{businessHandoffs.total} 项</span></div>
{businessHandoffs.items.map((item) => (
<Link className="mobile-list-card block" key={item.key} to={item.mobileHref}>
<div className="mobile-list-card__title">{item.title}{item.reference ? ` · ${item.reference}` : ""}</div>
<p className="mobile-list-card__subtitle mt-1">{item.nextStep}</p>
<p className="mt-1 text-xs text-zinc-500">{item.ownerLabel} · 查看处理</p>
</Link>
))}
</section>
) : null}
{shouldShowFinanceCard ? (
<section id="finance" tabIndex={-1} className="mobile-card mobile-page-stack" data-testid="mobile-finance-summary">
<div className="mobile-section-header">
@@ -305,10 +324,10 @@ export function MobilePortalPage() {
<span className="badge-neutral">{financeCardBadge}</span>
</div>
<div className="mobile-metric-grid text-sm">
<div className="rounded-lg bg-zinc-50 px-3 py-2">
{!businessHandoffs ? <div className="rounded-lg bg-zinc-50 px-3 py-2">
<div className="text-xs text-zinc-500">待付款</div>
<div className="mt-1 text-xl font-semibold text-zinc-900">{financePendingCount}</div>
</div>
</div> : null}
<div className="rounded-lg bg-zinc-50 px-3 py-2">
<div className="text-xs text-zinc-500">异常</div>
<div className="mt-1 text-xl font-semibold text-zinc-900">{financeExceptionCount}</div>
+11 -4
查看文件
@@ -28,6 +28,7 @@ type WorkItem = {
icon: string;
priority: number;
count: number;
showReason?: boolean;
};
type EmptyAction = {
@@ -107,6 +108,11 @@ function uniqueWorkItems(items: WorkItem[]) {
function buildTodayActionItems(payload: PortalHomePayload, roleCapabilities: RoleCapabilities) {
const items: WorkItem[] = [];
for (const handoff of payload.businessHandoffs?.items ?? []) {
items.push({ key: handoff.key, title: [handoff.title, handoff.reference].filter(Boolean).join(" · "),
reason: handoff.nextStep, meta: handoff.ownerLabel, href: handoff.href, actionLabel: "查看处理",
severity: "warning", icon: "assignment", priority: 98, count: 1, showReason: true });
}
if (payload.pendingTasks.count > 0) {
items.push({
@@ -158,7 +164,7 @@ function buildTodayActionItems(payload: PortalHomePayload, roleCapabilities: Rol
}
if (roleCapabilities.canUseFinance && payload.financeSummary) {
if (payload.financeSummary.failedItemCount > 0) {
if (!payload.businessHandoffs && payload.financeSummary.failedItemCount > 0) {
items.push({
key: "finance-failed-payment",
title: `异常付款 ${payload.financeSummary.failedItemCount} 项`,
@@ -188,7 +194,7 @@ function buildTodayActionItems(payload: PortalHomePayload, roleCapabilities: Rol
});
}
if (payload.financeSummary.pendingBatchCount > 0) {
if (!payload.businessHandoffs && payload.financeSummary.pendingBatchCount > 0) {
items.push({
key: "finance-pending-batch",
title: `待付款批次 ${payload.financeSummary.pendingBatchCount} 个`,
@@ -234,7 +240,7 @@ function buildTodayActionItems(payload: PortalHomePayload, roleCapabilities: Rol
});
}
return uniqueWorkItems(items).slice(0, 8);
return uniqueWorkItems(items);
}
function buildProgressItems(payload: PortalHomePayload) {
@@ -579,6 +585,7 @@ function WorkItemRow({ item }: { item: WorkItem }) {
</span>
<span className="min-w-0 flex-1">
<span className="portal-task-row__title">{item.title}</span>
{item.showReason ? <span className="mt-1 block text-sm text-zinc-600">{item.reason}</span> : null}
</span>
<span className="portal-task-row__meta">{item.meta}</span>
<span className="portal-task-row__action">{item.actionLabel}</span>
@@ -680,7 +687,7 @@ function ConditionalWorkPanel({
export function PortalHomePage() {
const { me, permissions } = useAuth();
const [showWelcome, setShowWelcome] = useState(false);
const portalQuery = useQuery({ queryKey: ["portal-home"], queryFn: () => fetchPortalHome() });
const portalQuery = useQuery({ queryKey: ["portal-home"], queryFn: () => fetchPortalHome(), staleTime: 0, refetchOnMount: true, refetchOnWindowFocus: true });
const canSetupWorkspace = hasAnyPermission(permissions, ["system.settings"]);
useEffect(() => {