feat(client): 行政代提交/代上传 owner 选择入口(F33)
- 报销创建支持选择 ownerEmployeeId(delegation-grants received) - 票据上传支持 owner_employee_id(delegation-grants received) - 保持普通员工流程不变,权限不足由后端阻断
这个提交包含在:
@@ -1,7 +1,9 @@
|
||||
import { useState } from "react";
|
||||
import { Link, useSearchParams } from "react-router-dom";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { isApiError } from "../api/types";
|
||||
import { mapApiErrorMessage } from "../api/errorMessages";
|
||||
import { fetchMyDelegations } from "../api/delegations";
|
||||
import { createReimbursement, fetchReimbursements, type ReimbursementStatus } from "../api/reimbursements";
|
||||
import { useAuth } from "../auth/AuthContext";
|
||||
import { hasPermission } from "../auth/permissions";
|
||||
@@ -15,21 +17,35 @@ const PAGE_SIZE = 20;
|
||||
|
||||
export function ReimbursementsPage() {
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const { permissions } = useAuth();
|
||||
const { permissions, me } = useAuth();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const status = (searchParams.get("status") as ReimbursementStatus | null) ?? "draft";
|
||||
const page = Number(searchParams.get("page") ?? "1") || 1;
|
||||
const scope = (searchParams.get("scope") as "own" | "all" | null) ?? "own";
|
||||
const canViewAll = hasPermission(permissions, "expense.view_all");
|
||||
const canProxySubmit = hasPermission(permissions, "expense.proxy_submit");
|
||||
const isAdministration = (me?.roleCodes ?? []).includes("administration");
|
||||
const showProxyOwnerSelector = canProxySubmit && isAdministration;
|
||||
|
||||
const [proxyOwnerEmployeeId, setProxyOwnerEmployeeId] = useState("");
|
||||
|
||||
const listQuery = useQuery({
|
||||
queryKey: ["reimbursements", "list", status, page, scope],
|
||||
queryFn: () => 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={
|
||||
<Button
|
||||
data-testid="reimbursements-create"
|
||||
disabled={createMutation.isPending}
|
||||
onClick={() => createMutation.mutate()}
|
||||
type="button"
|
||||
>
|
||||
{createMutation.isPending ? "创建中..." : "新建报销"}
|
||||
</Button>
|
||||
<div className="flex flex-col gap-2 sm:flex-row sm:items-center">
|
||||
{showProxyOwnerSelector ? (
|
||||
<select
|
||||
className="h-11 rounded-lg border border-slate-300 bg-white px-3 py-2 text-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}
|
||||
|
||||
<Button
|
||||
data-testid="reimbursements-create"
|
||||
disabled={createMutation.isPending}
|
||||
onClick={() => createMutation.mutate()}
|
||||
type="button"
|
||||
>
|
||||
{createMutation.isPending ? "创建中..." : "新建报销"}
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
|
||||
|
||||
在新工单中引用
屏蔽一个用户