R19/R20: 项目主域进 OA 并打通报销差旅周报待办
这个提交包含在:
@@ -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>
|
||||
|
||||
在新工单中引用
屏蔽一个用户