统一编辑模板并移除首批台账工具(05-31-business-edit-template-remove-ledger-tools-wave1)
这个提交包含在:
@@ -0,0 +1,162 @@
|
||||
import type { ReactNode } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { cn } from "../../lib/utils";
|
||||
import { Button } from "../ui/button";
|
||||
import { WorkspaceHeader } from "./ListWorkspace";
|
||||
import { SectionPanel } from "./SectionPanel";
|
||||
|
||||
export function BusinessEditTemplate({
|
||||
eyebrow,
|
||||
title,
|
||||
backTo,
|
||||
backLabel = "返回",
|
||||
status,
|
||||
actions,
|
||||
meta,
|
||||
validation,
|
||||
bottomActions,
|
||||
children,
|
||||
className,
|
||||
testId,
|
||||
}: {
|
||||
eyebrow?: string;
|
||||
title: string;
|
||||
backTo?: string;
|
||||
backLabel?: string;
|
||||
status?: ReactNode;
|
||||
actions?: ReactNode;
|
||||
meta?: ReactNode;
|
||||
validation?: ReactNode;
|
||||
bottomActions?: ReactNode;
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
testId?: string;
|
||||
}) {
|
||||
const headerActions = actions || status || backTo ? (
|
||||
<div className="flex flex-wrap items-center justify-end gap-2">
|
||||
{status ? <div className="shrink-0">{status}</div> : null}
|
||||
{actions}
|
||||
{backTo ? (
|
||||
<Button asChild type="button" variant="secondary">
|
||||
<Link to={backTo}>{backLabel}</Link>
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<div className={cn("page-scaffold", bottomActions && "pb-24 md:pb-0", className)} data-testid={testId}>
|
||||
<WorkspaceHeader eyebrow={eyebrow} title={title} meta={meta} actions={headerActions} />
|
||||
{validation ? <BusinessEditValidationSummary>{validation}</BusinessEditValidationSummary> : null}
|
||||
{children}
|
||||
{bottomActions ? <BusinessEditActionBar>{bottomActions}</BusinessEditActionBar> : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function BusinessEditWorkspace({
|
||||
children,
|
||||
className,
|
||||
testId,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
testId?: string;
|
||||
}) {
|
||||
return (
|
||||
<div className={cn("detail-workspace-grid", className)} data-testid={testId}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function BusinessEditMain({
|
||||
children,
|
||||
className,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
}) {
|
||||
return <div className={cn("space-y-5", className)}>{children}</div>;
|
||||
}
|
||||
|
||||
export function BusinessEditAside({
|
||||
children,
|
||||
className,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
}) {
|
||||
return <div className={cn("space-y-5", className)}>{children}</div>;
|
||||
}
|
||||
|
||||
export function BusinessEditSection({
|
||||
title,
|
||||
description,
|
||||
actions,
|
||||
children,
|
||||
className,
|
||||
}: {
|
||||
title: string;
|
||||
description?: string;
|
||||
actions?: ReactNode;
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<SectionPanel title={title} description={description} actions={actions} className={className}>
|
||||
{children}
|
||||
</SectionPanel>
|
||||
);
|
||||
}
|
||||
|
||||
export function BusinessEditFormGrid({
|
||||
children,
|
||||
className,
|
||||
columns = 2,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
columns?: 1 | 2 | 3;
|
||||
}) {
|
||||
const columnClass = {
|
||||
1: "grid-cols-1",
|
||||
2: "md:grid-cols-2",
|
||||
3: "md:grid-cols-2 xl:grid-cols-3",
|
||||
}[columns];
|
||||
|
||||
return <div className={cn("grid gap-4", columnClass, className)}>{children}</div>;
|
||||
}
|
||||
|
||||
export function BusinessEditValidationSummary({
|
||||
children,
|
||||
className,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<div className={cn("rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-700", className)}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function BusinessEditActionBar({
|
||||
children,
|
||||
className,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"mobile-sticky-action-surface fixed inset-x-0 bottom-0 z-50 border-t border-zinc-200 bg-white/95 shadow-[0_-10px_24px_rgba(15,23,42,0.10)] backdrop-blur md:static md:inset-auto md:mt-5 md:rounded-lg md:border md:bg-white md:p-3 md:shadow-none",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div className="mobile-sticky-action-grid md:flex md:justify-end md:gap-2">{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -226,7 +226,8 @@ export function AssetRegistryPage() {
|
||||
const [assigneeFilter, setAssigneeFilter] = useState("");
|
||||
const [assetSearch, setAssetSearch] = useState("");
|
||||
const [mine, setMine] = useState(!canViewAll);
|
||||
const [showLedgerTools, setShowLedgerTools] = useState(false);
|
||||
const [showCategoryManager, setShowCategoryManager] = useState(false);
|
||||
const [showAdvancedFilters, setShowAdvancedFilters] = useState(false);
|
||||
|
||||
const [selectedAssetId, setSelectedAssetId] = useState(searchParams.get("id") ?? "");
|
||||
const [isCreatingAsset, setIsCreatingAsset] = useState(false);
|
||||
@@ -377,6 +378,14 @@ export function AssetRegistryPage() {
|
||||
|
||||
const currentAsset = assetDetailQuery.data?.asset ?? null;
|
||||
const canEditCurrent = canManage && (isCreatingAsset || currentAsset?.permissions.canEdit);
|
||||
const hasAdvancedAssetFilters = Boolean(
|
||||
sensitivityFilter ||
|
||||
categoryFilter ||
|
||||
legalEntityFilter ||
|
||||
costCenterFilter ||
|
||||
assigneeFilter,
|
||||
);
|
||||
const showAssetAdvancedFilters = showAdvancedFilters || hasAdvancedAssetFilters;
|
||||
|
||||
function resetCategoryEditor(category?: AssetCategoryItem) {
|
||||
if (!category) {
|
||||
@@ -733,9 +742,15 @@ export function AssetRegistryPage() {
|
||||
<Button type="button" variant="secondary" onClick={() => navigate("/mobile/assets/scan") }>
|
||||
移动扫码查找
|
||||
</Button>
|
||||
<Button type="button" variant={showLedgerTools ? "default" : "secondary"} onClick={() => setShowLedgerTools((value) => !value)}>
|
||||
台账工具
|
||||
</Button>
|
||||
{canManage ? (
|
||||
<Button
|
||||
type="button"
|
||||
variant={showCategoryManager ? "default" : "secondary"}
|
||||
onClick={() => setShowCategoryManager((value) => !value)}
|
||||
>
|
||||
资产分类
|
||||
</Button>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
@@ -744,8 +759,8 @@ export function AssetRegistryPage() {
|
||||
|
||||
{!isAssetDetailMode ? (
|
||||
<div className="grid gap-6">
|
||||
{showLedgerTools ? (
|
||||
<div className="card-std space-y-4 p-4 md:p-5" data-testid="asset-ledger-tools">
|
||||
{showCategoryManager && canManage ? (
|
||||
<div className="card-std space-y-4 p-4 md:p-5" data-testid="asset-category-manager">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-base font-semibold text-zinc-900">资产分类</h3>
|
||||
{canManage ? (
|
||||
@@ -786,56 +801,6 @@ export function AssetRegistryPage() {
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="space-y-3 rounded-lg border border-zinc-200 bg-white p-3">
|
||||
<div>
|
||||
<h4 className="text-sm font-semibold text-zinc-900">高级筛选</h4>
|
||||
</div>
|
||||
<div className="grid gap-3">
|
||||
<select className="select-std" value={sensitivityFilter} onChange={(event) => setSensitivityFilter(event.target.value as AssetSensitivityLevel | "") }>
|
||||
{sensitivityOptions().map((item) => (
|
||||
<option key={item.value || "all"} value={item.value}>
|
||||
{item.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<select className="select-std" value={categoryFilter} onChange={(event) => setCategoryFilter(event.target.value)}>
|
||||
<option value="">全部分类</option>
|
||||
{(categoriesQuery.data?.items ?? []).map((item) => (
|
||||
<option key={item.id} value={item.id}>
|
||||
{formatTextWithCompactLongNumbers(item.name)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<select className="select-std" value={legalEntityFilter} onChange={(event) => {
|
||||
setLegalEntityFilter(event.target.value);
|
||||
setCostCenterFilter("");
|
||||
}}>
|
||||
<option value="">全部法体</option>
|
||||
{(legalEntitiesQuery.data?.items ?? []).map((item) => (
|
||||
<option key={item.id} value={item.id}>
|
||||
{formatTextWithCompactLongNumbers(item.name)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<select className="select-std" value={costCenterFilter} onChange={(event) => setCostCenterFilter(event.target.value)}>
|
||||
<option value="">全部成本中心</option>
|
||||
{filteredFilterCostCenters.map((item) => (
|
||||
<option key={item.id} value={item.id}>
|
||||
{item.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<select className="select-std" value={assigneeFilter} onChange={(event) => setAssigneeFilter(event.target.value)} disabled={!canViewAll}>
|
||||
<option value="">全部归属人</option>
|
||||
{(assigneesQuery.data?.items ?? []).map((item) => (
|
||||
<option key={item.id} value={item.id}>
|
||||
{item.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{categoriesQuery.isLoading ? <TableSkeleton columns={3} rows={5} /> : null}
|
||||
{categoriesQuery.isError ? (
|
||||
<p className="rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-600">{resolveError(categoriesQuery.error)}</p>
|
||||
@@ -903,15 +868,91 @@ export function AssetRegistryPage() {
|
||||
|
||||
<div className="grid gap-4">
|
||||
<BusinessListToolbar
|
||||
actions={canViewAll ? (
|
||||
actions={(
|
||||
<>
|
||||
<Button type="button" size="sm" variant={mine ? "default" : "secondary"} onClick={() => setMine(true)}>
|
||||
我相关
|
||||
</Button>
|
||||
<Button type="button" size="sm" variant={!mine ? "default" : "secondary"} onClick={() => setMine(false)}>
|
||||
全范围
|
||||
{canViewAll ? (
|
||||
<>
|
||||
<Button type="button" size="sm" variant={mine ? "default" : "secondary"} onClick={() => setMine(true)}>
|
||||
我相关
|
||||
</Button>
|
||||
<Button type="button" size="sm" variant={!mine ? "default" : "secondary"} onClick={() => setMine(false)}>
|
||||
全范围
|
||||
</Button>
|
||||
</>
|
||||
) : null}
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
variant={showAssetAdvancedFilters ? "default" : "secondary"}
|
||||
data-testid="asset-filter-toggle"
|
||||
onClick={() => setShowAdvancedFilters((value) => !value)}
|
||||
>
|
||||
筛选
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
advanced={showAssetAdvancedFilters ? (
|
||||
<div className="grid gap-3 md:grid-cols-2 xl:grid-cols-5">
|
||||
<select className="select-std" value={sensitivityFilter} onChange={(event) => setSensitivityFilter(event.target.value as AssetSensitivityLevel | "") }>
|
||||
{sensitivityOptions().map((item) => (
|
||||
<option key={item.value || "all"} value={item.value}>
|
||||
{item.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<select className="select-std" value={categoryFilter} onChange={(event) => setCategoryFilter(event.target.value)}>
|
||||
<option value="">全部分类</option>
|
||||
{(categoriesQuery.data?.items ?? []).map((item) => (
|
||||
<option key={item.id} value={item.id}>
|
||||
{formatTextWithCompactLongNumbers(item.name)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<select className="select-std" value={legalEntityFilter} onChange={(event) => {
|
||||
setLegalEntityFilter(event.target.value);
|
||||
setCostCenterFilter("");
|
||||
}}>
|
||||
<option value="">全部法体</option>
|
||||
{(legalEntitiesQuery.data?.items ?? []).map((item) => (
|
||||
<option key={item.id} value={item.id}>
|
||||
{formatTextWithCompactLongNumbers(item.name)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<select className="select-std" value={costCenterFilter} onChange={(event) => setCostCenterFilter(event.target.value)}>
|
||||
<option value="">全部成本中心</option>
|
||||
{filteredFilterCostCenters.map((item) => (
|
||||
<option key={item.id} value={item.id}>
|
||||
{item.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<select className="select-std" value={assigneeFilter} onChange={(event) => setAssigneeFilter(event.target.value)} disabled={!canViewAll}>
|
||||
<option value="">全部归属人</option>
|
||||
{(assigneesQuery.data?.items ?? []).map((item) => (
|
||||
<option key={item.id} value={item.id}>
|
||||
{item.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<div className="flex items-center md:col-span-2 xl:col-span-5">
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
setSensitivityFilter("");
|
||||
setCategoryFilter("");
|
||||
setLegalEntityFilter("");
|
||||
setCostCenterFilter("");
|
||||
setAssigneeFilter("");
|
||||
setShowAdvancedFilters(false);
|
||||
}}
|
||||
>
|
||||
重置筛选
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
>
|
||||
<div className="grid gap-3 md:grid-cols-[minmax(0,1fr)_180px_190px]">
|
||||
|
||||
+113
-116
@@ -1,7 +1,6 @@
|
||||
import { useMemo, useState } from "react";
|
||||
import { Link, useNavigate, useSearchParams } from "react-router-dom";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { Wrench } from "lucide-react";
|
||||
import { isApiError } from "../api/types";
|
||||
import { mapApiErrorMessage } from "../api/errorMessages";
|
||||
import { fetchMyDelegations } from "../api/delegations";
|
||||
@@ -154,7 +153,6 @@ export function ReimbursementsPage() {
|
||||
const [proxyOwnerEmployeeId, setProxyOwnerEmployeeId] = useState("");
|
||||
const [deleteTarget, setDeleteTarget] = useState<ReimbursementListItem | null>(null);
|
||||
const [exportConfirmFormat, setExportConfirmFormat] = useState<ExportFormat | null>(null);
|
||||
const [showLedgerTools, setShowLedgerTools] = useState(false);
|
||||
|
||||
const status = (searchParams.get("status") as ReimbursementStatus | null) ?? "draft";
|
||||
const page = Number(searchParams.get("page") ?? "1") || 1;
|
||||
@@ -170,9 +168,9 @@ export function ReimbursementsPage() {
|
||||
hasPermission(permissions, "invoice.manage");
|
||||
const isAdministration = (me?.roleCodes ?? []).includes("administration");
|
||||
const showProxyOwnerSelector = canProxySubmit && isAdministration;
|
||||
const canShowLedgerEntry = canViewAll || showProxyOwnerSelector || canUseFinanceTools;
|
||||
const showAdvancedTools = canShowLedgerEntry && (showLedgerTools || scope === "all" || showProxyOwnerSelector);
|
||||
const shouldLoadCreateOptions = showAdvancedTools && canCreateExpense;
|
||||
const canShowListActions = canViewAll || showProxyOwnerSelector || canUseFinanceTools;
|
||||
const showAdvancedTools = scope === "all" || showProxyOwnerSelector;
|
||||
const shouldLoadCreateOptions = canCreateExpense;
|
||||
|
||||
const listQuery = useQuery({
|
||||
queryKey: ["reimbursements", "list", status, page, scope],
|
||||
@@ -284,100 +282,27 @@ export function ReimbursementsPage() {
|
||||
title={scope === "all" ? "报销台账" : "我的报销"}
|
||||
actions={
|
||||
canCreateExpense ? (
|
||||
<Button
|
||||
data-testid="reimbursements-create"
|
||||
disabled={createMutation.isPending}
|
||||
onClick={() => startCreateReimbursement()}
|
||||
type="button"
|
||||
>
|
||||
{createMutation.isPending ? "创建中..." : "新建报销"}
|
||||
</Button>
|
||||
) : null
|
||||
}
|
||||
/>
|
||||
|
||||
{createMutation.error && isApiError(createMutation.error) ? (
|
||||
<p className="rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-600">
|
||||
{createMutation.error.code}:
|
||||
{mapApiErrorMessage(
|
||||
createMutation.error.code,
|
||||
createMutation.error.details,
|
||||
createMutation.error.message,
|
||||
)}
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
{exportError ? (
|
||||
<p className="rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-600">
|
||||
{exportError}
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
<BusinessListToolbar
|
||||
actions={
|
||||
canShowLedgerEntry ? (
|
||||
<Button
|
||||
size="sm"
|
||||
variant={showAdvancedTools ? "default" : "secondary"}
|
||||
onClick={() => setShowLedgerTools((current) => !current)}
|
||||
type="button"
|
||||
>
|
||||
<Wrench aria-hidden className="mr-1 size-4" />
|
||||
台账工具
|
||||
</Button>
|
||||
) : null
|
||||
}
|
||||
advanced={showAdvancedTools ? (
|
||||
<>
|
||||
<div className="grid gap-3 text-sm md:grid-cols-4">
|
||||
<div>
|
||||
<div className="text-xs text-zinc-500">当前列表</div>
|
||||
<div className="mt-1 font-semibold text-zinc-900">{stats.currentPageCount} 条 / 共 {listQuery.data?.total ?? 0} 条</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xs text-zinc-500">当前金额</div>
|
||||
<div className="mt-1 font-semibold text-zinc-900">{formatCurrency(stats.currentPageAmount)}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xs text-zinc-500">审批中</div>
|
||||
<div className={stats.reviewing > 0 ? "mt-1 font-semibold text-amber-700" : "mt-1 font-semibold text-zinc-900"}>{stats.reviewing} 条</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xs text-zinc-500">已支付</div>
|
||||
<div className="mt-1 font-semibold text-zinc-900">{stats.paid} 条</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-3 flex flex-wrap items-center gap-2">
|
||||
{showProxyOwnerSelector ? (
|
||||
<select
|
||||
className="select-sm"
|
||||
data-testid="reimbursements-owner-select"
|
||||
onChange={(event) => setProxyOwnerEmployeeId(event.target.value)}
|
||||
value={proxyOwnerEmployeeId}
|
||||
>
|
||||
<option value="">自己</option>
|
||||
{(proxyDelegationsQuery.data?.received ?? [])
|
||||
.filter((item) => item.status === "active")
|
||||
.map((item) => (
|
||||
<option key={item.id} value={item.principalEmployeeId}>
|
||||
{item.principal?.name ?? item.principalEmployeeId}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
) : null}
|
||||
|
||||
{canShowCreateOptionsMenu ? (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button size="sm" variant="secondary" type="button">
|
||||
更多创建方式
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-80">
|
||||
<div className="px-2 py-1 text-[11px] font-semibold uppercase tracking-[0.18em] text-zinc-400">
|
||||
从模板创建
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center justify-end gap-2">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
data-testid="reimbursements-create"
|
||||
disabled={createMutation.isPending}
|
||||
type="button"
|
||||
>
|
||||
{createMutation.isPending ? "创建中..." : "新建报销"}
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-80">
|
||||
<DropdownMenuItem onSelect={() => startCreateReimbursement()}>
|
||||
空白报销
|
||||
</DropdownMenuItem>
|
||||
{canShowCreateOptionsMenu ? (
|
||||
<>
|
||||
<DropdownMenuSeparator />
|
||||
<div className="px-2 py-1 text-[11px] font-semibold uppercase tracking-[0.18em] text-zinc-400">
|
||||
从模板创建
|
||||
</div>
|
||||
{templateOptions.length ? (
|
||||
templateOptions.map((template) => (
|
||||
<DropdownMenuItem
|
||||
@@ -404,13 +329,39 @@ export function ReimbursementsPage() {
|
||||
{travelRequest.requestNo ?? travelRequest.id} · {travelRequest.purpose}
|
||||
</DropdownMenuItem>
|
||||
))
|
||||
) : (
|
||||
<DropdownMenuItem disabled>当前无可关联申请</DropdownMenuItem>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
) : null}
|
||||
) : (
|
||||
<DropdownMenuItem disabled>当前无可关联申请</DropdownMenuItem>
|
||||
)}
|
||||
</>
|
||||
) : null}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
) : null
|
||||
}
|
||||
/>
|
||||
|
||||
{createMutation.error && isApiError(createMutation.error) ? (
|
||||
<p className="rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-600">
|
||||
{createMutation.error.code}:
|
||||
{mapApiErrorMessage(
|
||||
createMutation.error.code,
|
||||
createMutation.error.details,
|
||||
createMutation.error.message,
|
||||
)}
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
{exportError ? (
|
||||
<p className="rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-600">
|
||||
{exportError}
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
<BusinessListToolbar
|
||||
actions={
|
||||
canShowListActions ? (
|
||||
<>
|
||||
{canViewAll ? (
|
||||
<>
|
||||
<Button
|
||||
@@ -420,7 +371,7 @@ export function ReimbursementsPage() {
|
||||
onClick={() => setSearchParams({ status: "reviewing", page: "1", scope: "all" })}
|
||||
type="button"
|
||||
>
|
||||
审批中台账
|
||||
审批中
|
||||
</Button>
|
||||
<Button
|
||||
data-testid="reimbursements-scope-own"
|
||||
@@ -441,22 +392,22 @@ export function ReimbursementsPage() {
|
||||
全范围
|
||||
</Button>
|
||||
<Button asChild size="sm" variant="secondary" type="button">
|
||||
<Link to="/reimbursements/dashboard">统计分析</Link>
|
||||
<Link to="/reimbursements/dashboard">统计</Link>
|
||||
</Button>
|
||||
</>
|
||||
) : null}
|
||||
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
data-testid="reimbursements-export"
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
type="button"
|
||||
disabled={exportMutation.isPending}
|
||||
>
|
||||
{exportMutation.isPending ? "导出中..." : "导出"}
|
||||
</Button>
|
||||
<Button
|
||||
data-testid="reimbursements-export"
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
type="button"
|
||||
disabled={exportMutation.isPending}
|
||||
>
|
||||
{exportMutation.isPending ? "导出中..." : "导出"}
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem
|
||||
@@ -473,7 +424,53 @@ export function ReimbursementsPage() {
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</>
|
||||
) : null
|
||||
}
|
||||
advanced={showAdvancedTools ? (
|
||||
<>
|
||||
{scope === "all" ? (
|
||||
<div className="grid gap-3 text-sm md:grid-cols-4">
|
||||
<div>
|
||||
<div className="text-xs text-zinc-500">当前列表</div>
|
||||
<div className="mt-1 font-semibold text-zinc-900">{stats.currentPageCount} 条 / 共 {listQuery.data?.total ?? 0} 条</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xs text-zinc-500">当前金额</div>
|
||||
<div className="mt-1 font-semibold text-zinc-900">{formatCurrency(stats.currentPageAmount)}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xs text-zinc-500">审批中</div>
|
||||
<div className={stats.reviewing > 0 ? "mt-1 font-semibold text-amber-700" : "mt-1 font-semibold text-zinc-900"}>{stats.reviewing} 条</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xs text-zinc-500">已支付</div>
|
||||
<div className="mt-1 font-semibold text-zinc-900">{stats.paid} 条</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{showProxyOwnerSelector ? (
|
||||
<div className={scope === "all" ? "mt-3 flex flex-wrap items-center gap-2" : "flex flex-wrap items-center gap-2"}>
|
||||
{showProxyOwnerSelector ? (
|
||||
<select
|
||||
className="select-sm"
|
||||
data-testid="reimbursements-owner-select"
|
||||
onChange={(event) => setProxyOwnerEmployeeId(event.target.value)}
|
||||
value={proxyOwnerEmployeeId}
|
||||
>
|
||||
<option value="">自己</option>
|
||||
{(proxyDelegationsQuery.data?.received ?? [])
|
||||
.filter((item) => item.status === "active")
|
||||
.map((item) => (
|
||||
<option key={item.id} value={item.principalEmployeeId}>
|
||||
{item.principal?.name ?? item.principalEmployeeId}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
) : null}
|
||||
>
|
||||
|
||||
在新工单中引用
屏蔽一个用户