From b341e2fa63e40c8097cfe2d6ab10be10bea3b380 Mon Sep 17 00:00:00 2001 From: shunli Date: Mon, 9 Feb 2026 18:32:38 +0800 Subject: [PATCH] =?UTF-8?q?feat(client):=20=E8=A1=8C=E6=94=BF=E4=BB=A3?= =?UTF-8?q?=E6=8F=90=E4=BA=A4/=E4=BB=A3=E4=B8=8A=E4=BC=A0=20owner=20?= =?UTF-8?q?=E9=80=89=E6=8B=A9=E5=85=A5=E5=8F=A3(F33)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 报销创建支持选择 ownerEmployeeId(delegation-grants received) - 票据上传支持 owner_employee_id(delegation-grants received) - 保持普通员工流程不变,权限不足由后端阻断 --- client/src/api/delegations.ts | 17 +++++++- client/src/api/invoicePool.ts | 5 ++- client/src/api/reimbursements.ts | 1 + client/src/pages/InvoicePoolPage.tsx | 32 +++++++++++++- client/src/pages/ReimbursementsPage.tsx | 56 ++++++++++++++++++++----- 5 files changed, 97 insertions(+), 14 deletions(-) diff --git a/client/src/api/delegations.ts b/client/src/api/delegations.ts index 53947156..6e015521 100644 --- a/client/src/api/delegations.ts +++ b/client/src/api/delegations.ts @@ -12,6 +12,22 @@ export type DelegationItem = { revokedAt: string | null; createdAt: string; status: DelegationStatus; + principal?: { + id: string; + employeeNo: string; + name: string; + departmentId: string; + status: string; + roleCodes: string[]; + } | null; + delegate?: { + id: string; + employeeNo: string; + name: string; + departmentId: string; + status: string; + roleCodes: string[]; + } | null; }; export type CreateDelegationRequest = { @@ -56,4 +72,3 @@ export async function revokeDelegation(delegationId: string) { }, ); } - diff --git a/client/src/api/invoicePool.ts b/client/src/api/invoicePool.ts index e1bc4460..0b10df28 100644 --- a/client/src/api/invoicePool.ts +++ b/client/src/api/invoicePool.ts @@ -96,12 +96,15 @@ export async function fetchInvoicePoolDetail(invoiceId: string) { return apiRequestData(`/api/invoice-pool/${invoiceId}`); } -export async function uploadInvoice(file: File, ocrType?: string) { +export async function uploadInvoice(file: File, ocrType?: string, ownerEmployeeId?: string) { const formData = new FormData(); formData.append("file", file); if (ocrType) { formData.append("ocr_type", ocrType); } + if (ownerEmployeeId) { + formData.append("owner_employee_id", ownerEmployeeId); + } return apiRequestData<{ invoicePoolId: string; status: string; fileSha256: string }>( "/api/invoice-pool/upload", diff --git a/client/src/api/reimbursements.ts b/client/src/api/reimbursements.ts index 4aaa9a40..98fe6709 100644 --- a/client/src/api/reimbursements.ts +++ b/client/src/api/reimbursements.ts @@ -160,6 +160,7 @@ export async function fetchReimbursementDetail(reimbursementId: string) { } export async function createReimbursement(payload: { + ownerEmployeeId?: string; reimbursementDate?: string; description?: string; } = {}) { diff --git a/client/src/pages/InvoicePoolPage.tsx b/client/src/pages/InvoicePoolPage.tsx index e53892e0..b35d0085 100644 --- a/client/src/pages/InvoicePoolPage.tsx +++ b/client/src/pages/InvoicePoolPage.tsx @@ -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(""); const [ownerScope, setOwnerScope] = useState<"own" | "all">("own"); const [page, setPage] = useState(1); const [selectedFile, setSelectedFile] = useState(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() {

上传票据(VAT OCR)

+ {showProxyOwnerSelector ? ( + + ) : null} fetchReimbursements({ status, page, pageSize: PAGE_SIZE, scope }), }); + const proxyDelegationsQuery = useQuery({ + enabled: showProxyOwnerSelector, + queryKey: ["delegations", "mine", "active"], + queryFn: () => fetchMyDelegations("active"), + }); + const createMutation = useMutation({ - mutationFn: () => createReimbursement({}), + mutationFn: () => + createReimbursement({ + ownerEmployeeId: proxyOwnerEmployeeId || undefined, + }), onSuccess: async () => { await queryClient.invalidateQueries({ queryKey: ["reimbursements"] }); }, @@ -43,14 +59,34 @@ export function ReimbursementsPage() { title="我的报销" subtitle="报销列表、详情与提交/撤回/支付动作入口。" actions={ - +
+ {showProxyOwnerSelector ? ( + + ) : null} + + +
} />