feat(P25-2/P25-3/P26-2/P28-1/P24-2/P29-3): 落地内部OA够用版前端入口与运营页面
关联 Issue: P25-2, P25-3, P25-4, P26-2, P26-4, P28-1, P28-2, P28-3, P28-4, P24-2, P29-3
这个提交包含在:
@@ -10,6 +10,7 @@ import {
|
||||
deleteReimbursementItem,
|
||||
fetchReimbursementDetail,
|
||||
payReimbursement,
|
||||
precheckReimbursement,
|
||||
submitReimbursement,
|
||||
updateReimbursementItem,
|
||||
withdrawReimbursement,
|
||||
@@ -21,6 +22,7 @@ import { hasPermission } from "../auth/permissions";
|
||||
import { CardSkeleton } from "../components/common/CardSkeleton";
|
||||
import { ConfirmDialog } from "../components/common/ConfirmDialog";
|
||||
import { PageHeader } from "../components/common/PageHeader";
|
||||
import { RiskFlagsCard } from "../components/common/RiskFlagsCard";
|
||||
import { AiSuggestionCard } from "../components/ai/AiSuggestionCard";
|
||||
import { Button } from "../components/ui/button";
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "../components/ui/table";
|
||||
@@ -30,6 +32,7 @@ import {
|
||||
type ItemEditorInitialValues,
|
||||
type ItemEditorSaveData,
|
||||
} from "./reimbursement-detail/ReimbursementItemEditor";
|
||||
import { buildPrecheckDialogMessage } from "../lib/precheck";
|
||||
|
||||
type ItemEditorState = { mode: "create" } | { mode: "edit"; itemId: string } | null;
|
||||
|
||||
@@ -319,6 +322,46 @@ export function ReimbursementDetailPage() {
|
||||
setItemError("");
|
||||
};
|
||||
|
||||
const handleSubmitAction = async () => {
|
||||
if (!reimbursementId) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setActionError("");
|
||||
const precheck = await precheckReimbursement(reimbursementId);
|
||||
const message = buildPrecheckDialogMessage(precheck);
|
||||
|
||||
if (precheck.decision === "block") {
|
||||
setActionError(message);
|
||||
toast.error("提交前校验未通过");
|
||||
return;
|
||||
}
|
||||
|
||||
if ((precheck.decision === "warn" || precheck.decision === "manual_review") && !window.confirm(message)) {
|
||||
return;
|
||||
}
|
||||
|
||||
mutation.mutate({ action: "submit" });
|
||||
} catch (error) {
|
||||
if (isApiError(error)) {
|
||||
const message = `${error.code}: ${mapApiErrorMessage(error.code, error.details, error.message)}`;
|
||||
setActionError(message);
|
||||
toast.error(message);
|
||||
return;
|
||||
}
|
||||
|
||||
if (error instanceof Error) {
|
||||
setActionError(error.message);
|
||||
toast.error(error.message);
|
||||
return;
|
||||
}
|
||||
|
||||
setActionError("提交前校验失败,请稍后重试");
|
||||
toast.error("提交前校验失败,请稍后重试");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<PageHeader
|
||||
@@ -414,6 +457,8 @@ export function ReimbursementDetailPage() {
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<RiskFlagsCard items={detail.riskFlags} title="待处理风险项" />
|
||||
|
||||
<AiSuggestionCard targetType="reimbursement" targetId={detail.id} />
|
||||
|
||||
<div className="card-std space-y-4 p-6" data-testid="reimbursement-items">
|
||||
@@ -528,7 +573,7 @@ export function ReimbursementDetailPage() {
|
||||
canPay={canPay}
|
||||
actionError={actionError}
|
||||
isPending={mutation.isPending}
|
||||
onSubmit={() => mutation.mutate({ action: "submit" })}
|
||||
onSubmit={() => void handleSubmitAction()}
|
||||
onWithdraw={(comment) => mutation.mutate({ action: "withdraw", comment })}
|
||||
onPay={(payload) => mutation.mutate({ action: "pay", payload })}
|
||||
/>
|
||||
|
||||
在新工单中引用
屏蔽一个用户