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

这个提交包含在:
2026-03-22 08:34:18 +08:00
父节点 0235ccb968
当前提交 3f9738ac9c
共修改 28 个文件,包含 3236 行新增和 21 行删除
@@ -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>