feat(client): 行政代提交/代上传 owner 选择入口(F33)

- 报销创建支持选择 ownerEmployeeId(delegation-grants received)
- 票据上传支持 owner_employee_id(delegation-grants received)
- 保持普通员工流程不变,权限不足由后端阻断
这个提交包含在:
2026-02-09 18:32:38 +08:00
父节点 504ff02dd5
当前提交 b341e2fa63
共修改 5 个文件,包含 97 行新增和 14 行删除
+30 -2
查看文件
@@ -2,6 +2,7 @@ import { useEffect, useState } from "react";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { isApiError } from "../api/types";
import { mapApiErrorMessage } from "../api/errorMessages";
import { fetchMyDelegations } from "../api/delegations";
import {
bindInvoice,
fetchInvoicePoolDetail,
@@ -22,12 +23,16 @@ const PAGE_SIZE = 20;
export function InvoicePoolPage() {
const queryClient = useQueryClient();
const { permissions } = useAuth();
const { permissions, me } = useAuth();
const canManageInvoice = hasPermission(permissions, "invoice.manage");
const canProxySubmit = hasPermission(permissions, "expense.proxy_submit");
const isAdministration = (me?.roleCodes ?? []).includes("administration");
const showProxyOwnerSelector = canProxySubmit && isAdministration;
const [status, setStatus] = useState<InvoicePoolStatus | "">("");
const [ownerScope, setOwnerScope] = useState<"own" | "all">("own");
const [page, setPage] = useState(1);
const [selectedFile, setSelectedFile] = useState<File | null>(null);
const [uploadOwnerEmployeeId, setUploadOwnerEmployeeId] = useState("");
const [selectedInvoiceId, setSelectedInvoiceId] = useState("");
const [selectedReimbursementId, setSelectedReimbursementId] = useState("");
const [detailInvoiceId, setDetailInvoiceId] = useState("");
@@ -49,6 +54,12 @@ export function InvoicePoolPage() {
queryFn: () => fetchReimbursements({ page: 1, pageSize: 50, scope: "own" }),
});
const proxyDelegationsQuery = useQuery({
enabled: showProxyOwnerSelector,
queryKey: ["delegations", "mine", "active"],
queryFn: () => fetchMyDelegations("active"),
});
const detailQuery = useQuery({
enabled: Boolean(detailInvoiceId),
queryKey: ["invoice-pool", "detail", detailInvoiceId],
@@ -67,7 +78,7 @@ export function InvoicePoolPage() {
if (!selectedFile) {
throw new Error("请选择文件");
}
return uploadInvoice(selectedFile, "vat");
return uploadInvoice(selectedFile, "vat", uploadOwnerEmployeeId || undefined);
},
onSuccess: async (data) => {
await queryClient.invalidateQueries({ queryKey: ["invoice-pool"] });
@@ -151,6 +162,23 @@ export function InvoicePoolPage() {
<Card className="space-y-3 p-6">
<h3 className="text-base font-semibold text-slate-900">上传票据(VAT OCR)</h3>
{showProxyOwnerSelector ? (
<select
className="h-11 w-full rounded-lg border border-slate-300 bg-white px-3 py-2 text-sm"
data-testid="invoice-upload-owner"
onChange={(event) => setUploadOwnerEmployeeId(event.target.value)}
value={uploadOwnerEmployeeId}
>
<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}
<input
accept="application/pdf,image/jpeg,image/png,image/webp"
data-testid="invoice-upload-file"