R19/R20: 项目主域进 OA 并打通报销差旅周报待办
这个提交包含在:
@@ -5,6 +5,7 @@ import { isApiError } from "../api/types";
|
||||
import { mapApiErrorMessage } from "../api/errorMessages";
|
||||
import { fetchExpenseTypes } from "../api/expenseTypes";
|
||||
import { exportReimbursementJournal, previewReimbursementJournal } from "../api/integrations";
|
||||
import { fetchProjects } from "../api/projects";
|
||||
import {
|
||||
createReimbursementItem,
|
||||
deleteReimbursementItem,
|
||||
@@ -14,6 +15,7 @@ import {
|
||||
submitReimbursement,
|
||||
updateReimbursementItem,
|
||||
withdrawReimbursement,
|
||||
linkReimbursementProject,
|
||||
type ReimbursementItem,
|
||||
} from "../api/reimbursements";
|
||||
import { toast } from "sonner";
|
||||
@@ -69,6 +71,10 @@ export function ReimbursementDetailPage() {
|
||||
queryFn: () => fetchExpenseTypes({ enabled: true }),
|
||||
});
|
||||
|
||||
const projectsQuery = useQuery({
|
||||
queryKey: ["projects", "reimbursement-link"],
|
||||
queryFn: () => fetchProjects({ mine: true }),
|
||||
});
|
||||
|
||||
const canManageIntegration = hasPermission(permissions, "integration.manage");
|
||||
|
||||
@@ -211,6 +217,27 @@ export function ReimbursementDetailPage() {
|
||||
},
|
||||
});
|
||||
|
||||
const projectLinkMutation = useMutation({
|
||||
mutationFn: (projectId: string | null) => linkReimbursementProject(reimbursementId, projectId),
|
||||
onSuccess: async () => {
|
||||
await Promise.all([
|
||||
queryClient.invalidateQueries({ queryKey: ["reimbursements"] }),
|
||||
queryClient.invalidateQueries({ queryKey: ["reimbursements", reimbursementId] }),
|
||||
queryClient.invalidateQueries({ queryKey: ["projects"] }),
|
||||
queryClient.invalidateQueries({ queryKey: ["portal-home"] }),
|
||||
]);
|
||||
toast.success("已更新关联项目");
|
||||
},
|
||||
onError: (error) => {
|
||||
const message = isApiError(error)
|
||||
? `${error.code}: ${mapApiErrorMessage(error.code, error.details, error.message)}`
|
||||
: error instanceof Error
|
||||
? error.message
|
||||
: "关联项目失败";
|
||||
toast.error(message);
|
||||
},
|
||||
});
|
||||
|
||||
const erpExportMutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
if (!reimbursementId) {
|
||||
@@ -383,6 +410,29 @@ export function ReimbursementDetailPage() {
|
||||
<div>说明:{detail.description || "-"}</div>
|
||||
</div>
|
||||
|
||||
<div className="card-std space-y-3 p-6 text-sm">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div>
|
||||
<h3 className="text-base font-semibold text-zinc-900">关联项目</h3>
|
||||
<p className="text-xs text-zinc-500">报销可选挂到项目,用于项目执行面板汇总。</p>
|
||||
</div>
|
||||
{detail.project ? <Link to={`/projects/${detail.project.id}`} className="text-sm text-primary hover:underline">查看项目</Link> : null}
|
||||
</div>
|
||||
<select
|
||||
className="select-std 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>
|
||||
|
||||
{budgetHint ? (
|
||||
<div className="card-std space-y-2 p-6 text-sm" data-testid="reimbursement-budget-hint">
|
||||
<h3 className="text-base font-semibold text-zinc-900">预算提示</h3>
|
||||
|
||||
在新工单中引用
屏蔽一个用户