R19/R20: 项目主域进 OA 并打通报销差旅周报待办

这个提交包含在:
2026-03-22 08:34:18 +08:00
父节点 0235ccb968
当前提交 3f9738ac9c
共修改 28 个文件,包含 3236 行新增和 21 行删除
+46
查看文件
@@ -2,6 +2,7 @@ import { useEffect, useMemo, useState, type Dispatch, type SetStateAction } from
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { useSearchParams } from "react-router-dom";
import { toast } from "sonner";
import { fetchProjects } from "../api/projects";
import { mapApiErrorMessage } from "../api/errorMessages";
import { isApiError } from "../api/types";
import {
@@ -12,6 +13,7 @@ import {
submitTravelRequest,
updateTravelRequest,
withdrawTravelRequest,
linkTravelRequestProject,
type TravelRequestDetail,
type TravelRequestPayload,
type TravelRequestStatus,
@@ -250,6 +252,12 @@ export function TravelRequestsPage() {
queryFn: () => fetchTravelRequestDetail(selectedId),
});
const projectsQuery = useQuery({
enabled: canAccessRequestsTab && activeTab === "requests",
queryKey: ["projects", "travel-link"],
queryFn: () => fetchProjects({ mine: true }),
});
const selectableCostCenters = useMemo(() => {
return (costCentersQuery.data?.items ?? []).filter((item) => {
if (!legalEntityId) {
@@ -413,6 +421,20 @@ export function TravelRequestsPage() {
}
}
const projectLinkMutation = useMutation({
mutationFn: (projectId: string | null) => linkTravelRequestProject(selectedId, projectId),
onSuccess: async () => {
await Promise.all([
queryClient.invalidateQueries({ queryKey: ["travel-requests"] }),
queryClient.invalidateQueries({ queryKey: ["travel-requests", "detail", selectedId] }),
queryClient.invalidateQueries({ queryKey: ["projects"] }),
queryClient.invalidateQueries({ queryKey: ["portal-home"] }),
]);
toast.success("已更新关联项目");
},
onError: (error) => toast.error(resolveError(error)),
});
const saveMutation = useMutation({
mutationFn: async () => {
validateForm();
@@ -650,6 +672,7 @@ export function TravelRequestsPage() {
<div>{tripTypeLabel(item.tripType)} · {item.startDate ?? "-"} → {item.endDate ?? "-"}</div>
<div>预估金额:{item.estimatedAmount}</div>
<div>申请人:{item.applicant?.name ?? "-"}</div>
{item.project ? <div>项目:{item.project.projectNo} · {item.project.name}</div> : null}
</div>
</button>
))
@@ -756,6 +779,29 @@ export function TravelRequestsPage() {
<RiskFlagsCard items={detail.riskFlags} title="待处理风险项" />
<div className="card-std border border-zinc-200 p-4">
<div className="flex items-center justify-between gap-3">
<div>
<div className="font-medium text-zinc-900">关联项目</div>
<div className="text-xs text-zinc-500">差旅申请可选挂到项目,用于费用和出差统一汇总。</div>
</div>
{detail.project ? <a href={`/projects/${detail.project.id}`} className="text-sm text-primary hover:underline">查看项目</a> : null}
</div>
<select
className="select-std mt-3 max-w-xl"
value={detail.projectId ?? ""}
onChange={(event) => projectLinkMutation.mutate(event.target.value || null)}
disabled={projectLinkMutation.isPending}
>
<option value="">不关联项目</option>
{(projectsQuery.data?.items ?? []).map((item) => (
<option key={item.id} value={item.id}>
{item.projectNo} · {item.name}
</option>
))}
</select>
</div>
<div className="grid gap-3 md:grid-cols-2 xl:grid-cols-4 text-sm">
<div>
<p className="text-zinc-500">申请人</p>