R7E-16E/R7E-16F 高风险操作确认统一
这个提交包含在:
@@ -34,6 +34,7 @@ import { fetchEmployeePerformanceTimeline } from "../api/performance";
|
||||
import { useAuth } from "../auth/AuthContext";
|
||||
import { hasPermission } from "../auth/permissions";
|
||||
import { EmployeeAvatar } from "../components/common/EmployeeAvatar";
|
||||
import { ActionConfirmDialog } from "../components/common/ActionConfirmDialog";
|
||||
import { PageHeader } from "../components/common/PageHeader";
|
||||
import { EmployeePerformancePanel } from "../components/people/EmployeePerformancePanel";
|
||||
import { EmployeeSalaryPanel } from "../components/people/EmployeeSalaryPanel";
|
||||
@@ -178,6 +179,8 @@ export function EmployeeProfilePage() {
|
||||
const [trainingForm, setTrainingForm] = useState<TrainingFormState>(buildEmptyTrainingForm());
|
||||
const [editingCertificateId, setEditingCertificateId] = useState<string | null>(null);
|
||||
const [certificateForm, setCertificateForm] = useState<CertificateFormState>(buildEmptyCertificateForm());
|
||||
const [deleteAvatarConfirmOpen, setDeleteAvatarConfirmOpen] = useState(false);
|
||||
const [deleteContractTarget, setDeleteContractTarget] = useState<EmployeeContract | null>(null);
|
||||
const [form, setForm] = useState({
|
||||
title: "",
|
||||
hireDate: "",
|
||||
@@ -644,10 +647,7 @@ export function EmployeeProfilePage() {
|
||||
}
|
||||
|
||||
function handleDeleteAvatar() {
|
||||
if (!window.confirm("确认删除当前头像?")) {
|
||||
return;
|
||||
}
|
||||
deleteAvatarMutation.mutate();
|
||||
setDeleteAvatarConfirmOpen(true);
|
||||
}
|
||||
|
||||
function handleDeleteContract(contract: EmployeeContract) {
|
||||
@@ -655,13 +655,11 @@ export function EmployeeProfilePage() {
|
||||
toast.error("生效中的合同需先终止后再删除");
|
||||
return;
|
||||
}
|
||||
if (!window.confirm(`确认删除合同 ${contract.contractNo}?`)) {
|
||||
return;
|
||||
}
|
||||
deleteContractMutation.mutate(contract.id);
|
||||
setDeleteContractTarget(contract);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="space-y-6">
|
||||
<PageHeader
|
||||
title={`${profile.employee.name} 档案`}
|
||||
@@ -1437,6 +1435,31 @@ export function EmployeeProfilePage() {
|
||||
) : null}
|
||||
</Tabs>
|
||||
</div>
|
||||
<ActionConfirmDialog
|
||||
open={deleteAvatarConfirmOpen}
|
||||
onOpenChange={setDeleteAvatarConfirmOpen}
|
||||
kind="delete"
|
||||
targetName="当前头像"
|
||||
description="删除后个人档案将不再显示当前头像,需要重新上传才能恢复。"
|
||||
onConfirm={async () => {
|
||||
deleteAvatarMutation.mutate();
|
||||
}}
|
||||
/>
|
||||
<ActionConfirmDialog
|
||||
open={Boolean(deleteContractTarget)}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) setDeleteContractTarget(null);
|
||||
}}
|
||||
kind="delete"
|
||||
targetName={deleteContractTarget ? `合同 ${deleteContractTarget.contractNo}` : undefined}
|
||||
description="删除后该合同记录和附件引用将从员工档案中移除,生效中的合同不能删除。"
|
||||
onConfirm={async () => {
|
||||
if (deleteContractTarget) {
|
||||
deleteContractMutation.mutate(deleteContractTarget.id);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ import { mapApiErrorMessage } from "../api/errorMessages";
|
||||
import { useAuth } from "../auth/AuthContext";
|
||||
import { hasPermission } from "../auth/permissions";
|
||||
import { ListWorkspace, WorkspaceHeader } from "../components/patterns/ListWorkspace";
|
||||
import { ActionConfirmDialog } from "../components/common/ActionConfirmDialog";
|
||||
import { Button } from "../components/ui/button";
|
||||
import { Input } from "../components/ui/input";
|
||||
import { Label } from "../components/ui/label";
|
||||
@@ -61,6 +62,11 @@ export function PaymentBatchesPage() {
|
||||
const [defaultPaymentChannel, setDefaultPaymentChannel] = useState<PaymentChannel | "">("");
|
||||
const [voucherPrefix, setVoucherPrefix] = useState("");
|
||||
const [error, setError] = useState("");
|
||||
const [confirmPaymentAction, setConfirmPaymentAction] = useState<{
|
||||
kind: "financeReview" | "financePayment" | "cancel";
|
||||
batchId: string;
|
||||
batchNo: string;
|
||||
} | null>(null);
|
||||
|
||||
const listQuery = useQuery({
|
||||
queryKey: ["payment-batches"],
|
||||
@@ -330,7 +336,11 @@ export function PaymentBatchesPage() {
|
||||
type="button"
|
||||
variant="secondary"
|
||||
disabled={reviewMutation.isPending || !canReviewSelectedBatch}
|
||||
onClick={() => reviewMutation.mutate(selectedBatch.id)}
|
||||
onClick={() => setConfirmPaymentAction({
|
||||
kind: "financeReview",
|
||||
batchId: selectedBatch.id,
|
||||
batchNo: selectedBatch.batchNo,
|
||||
})}
|
||||
>
|
||||
{selectedBatch.reviewRequirement.nextStep === "second_review" ? "二次复核" : "复核"}
|
||||
</Button>
|
||||
@@ -339,7 +349,11 @@ export function PaymentBatchesPage() {
|
||||
<Button
|
||||
type="button"
|
||||
disabled={executeMutation.isPending || !canExecuteSelectedBatch}
|
||||
onClick={() => executeMutation.mutate(selectedBatch.id)}
|
||||
onClick={() => setConfirmPaymentAction({
|
||||
kind: "financePayment",
|
||||
batchId: selectedBatch.id,
|
||||
batchNo: selectedBatch.batchNo,
|
||||
})}
|
||||
>
|
||||
执行
|
||||
</Button>
|
||||
@@ -349,7 +363,11 @@ export function PaymentBatchesPage() {
|
||||
type="button"
|
||||
variant="secondary"
|
||||
disabled={cancelMutation.isPending || !canCancelSelectedBatch}
|
||||
onClick={() => cancelMutation.mutate(selectedBatch.id)}
|
||||
onClick={() => setConfirmPaymentAction({
|
||||
kind: "cancel",
|
||||
batchId: selectedBatch.id,
|
||||
batchNo: selectedBatch.batchNo,
|
||||
})}
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
@@ -417,6 +435,27 @@ export function PaymentBatchesPage() {
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
<ActionConfirmDialog
|
||||
open={Boolean(confirmPaymentAction)}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) setConfirmPaymentAction(null);
|
||||
}}
|
||||
kind={confirmPaymentAction?.kind ?? "financePayment"}
|
||||
targetName={confirmPaymentAction?.batchNo}
|
||||
description={
|
||||
confirmPaymentAction?.kind === "financeReview"
|
||||
? "确认后将写入付款批次复核记录;高风险批次仍需满足双人复核要求。"
|
||||
: confirmPaymentAction?.kind === "cancel"
|
||||
? "取消后该付款批次不能继续执行,历史记录会保留。"
|
||||
: "确认后将执行付款批次并影响付款状态,请确认批次、金额和复核要求均已核对。"
|
||||
}
|
||||
onConfirm={async () => {
|
||||
if (!confirmPaymentAction) return;
|
||||
if (confirmPaymentAction.kind === "financeReview") reviewMutation.mutate(confirmPaymentAction.batchId);
|
||||
if (confirmPaymentAction.kind === "financePayment") executeMutation.mutate(confirmPaymentAction.batchId);
|
||||
if (confirmPaymentAction.kind === "cancel") cancelMutation.mutate(confirmPaymentAction.batchId);
|
||||
}}
|
||||
/>
|
||||
</ListWorkspace>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import { SectionPanel } from "../components/patterns/SectionPanel";
|
||||
import { isApiError } from "../api/types";
|
||||
import { StatusBadge } from "../components/common/StatusBadge";
|
||||
import { Button } from "../components/ui/button";
|
||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "../components/ui/dialog";
|
||||
import { Input } from "../components/ui/input";
|
||||
import { Label } from "../components/ui/label";
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "../components/ui/table";
|
||||
@@ -173,6 +174,8 @@ export function ProjectDetailPage() {
|
||||
nextFollowUpDate: "",
|
||||
});
|
||||
const [editingCommunicationLogId, setEditingCommunicationLogId] = useState<string | null>(null);
|
||||
const [waiveChecklistTarget, setWaiveChecklistTarget] = useState<{ itemId: string; currentNote: string | null } | null>(null);
|
||||
const [waiveChecklistNote, setWaiveChecklistNote] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
if (!detail) return;
|
||||
@@ -452,15 +455,8 @@ export function ProjectDetailPage() {
|
||||
|
||||
const handleChecklistStatusChange = (itemId: string, status: ProjectClosureChecklistStatus, currentNote: string | null) => {
|
||||
if (status === "waived") {
|
||||
const note = window.prompt("请填写豁免理由", currentNote ?? "");
|
||||
if (note == null) {
|
||||
return;
|
||||
}
|
||||
if (!note.trim()) {
|
||||
toast.error("豁免必须填写说明");
|
||||
return;
|
||||
}
|
||||
patchClosureChecklistMutation.mutate({ itemId, status, note: note.trim() });
|
||||
setWaiveChecklistTarget({ itemId, currentNote });
|
||||
setWaiveChecklistNote(currentNote ?? "");
|
||||
return;
|
||||
}
|
||||
patchClosureChecklistMutation.mutate({ itemId, status });
|
||||
@@ -491,6 +487,7 @@ export function ProjectDetailPage() {
|
||||
const riskItems = risksQuery.data?.items ?? detail.risks;
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageScaffold>
|
||||
<EntityOverviewHero
|
||||
title={`${detail.projectNo} · ${detail.name}`}
|
||||
@@ -1061,6 +1058,70 @@ export function ProjectDetailPage() {
|
||||
</div>
|
||||
</div>
|
||||
</PageScaffold>
|
||||
<Dialog
|
||||
open={Boolean(waiveChecklistTarget)}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
setWaiveChecklistTarget(null);
|
||||
setWaiveChecklistNote("");
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DialogContent className="max-w-md" data-testid="action-confirm-dialog">
|
||||
<DialogHeader>
|
||||
<DialogTitle>确认豁免收口项</DialogTitle>
|
||||
<DialogDescription>
|
||||
豁免后该收口项会标记为已豁免,并写入项目收口记录。必须填写可理解说明。
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-zinc-900" htmlFor="project-closure-waive-note">
|
||||
豁免理由
|
||||
</label>
|
||||
<Textarea
|
||||
id="project-closure-waive-note"
|
||||
data-testid="project-closure-waive-note"
|
||||
value={waiveChecklistNote}
|
||||
onChange={(event) => setWaiveChecklistNote(event.target.value)}
|
||||
placeholder="说明为什么该收口项可以豁免"
|
||||
rows={3}
|
||||
/>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
data-testid="action-confirm-cancel"
|
||||
disabled={patchClosureChecklistMutation.isPending}
|
||||
onClick={() => {
|
||||
setWaiveChecklistTarget(null);
|
||||
setWaiveChecklistNote("");
|
||||
}}
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="destructive"
|
||||
data-testid="action-confirm-submit"
|
||||
disabled={!waiveChecklistTarget || !waiveChecklistNote.trim() || patchClosureChecklistMutation.isPending}
|
||||
onClick={() => {
|
||||
if (!waiveChecklistTarget) return;
|
||||
patchClosureChecklistMutation.mutate({
|
||||
itemId: waiveChecklistTarget.itemId,
|
||||
status: "waived",
|
||||
note: waiveChecklistNote.trim(),
|
||||
});
|
||||
setWaiveChecklistTarget(null);
|
||||
setWaiveChecklistNote("");
|
||||
}}
|
||||
>
|
||||
{patchClosureChecklistMutation.isPending ? "处理中..." : "确认豁免"}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@ import { useAuth } from "../auth/AuthContext";
|
||||
import { hasPermission } from "../auth/permissions";
|
||||
import { RiskFlagsCard } from "../components/common/RiskFlagsCard";
|
||||
import { AiSuggestionCard } from "../components/ai/AiSuggestionCard";
|
||||
import { ActionConfirmDialog } from "../components/common/ActionConfirmDialog";
|
||||
import { Pagination } from "../components/common/Pagination";
|
||||
import { StatusBadge } from "../components/common/StatusBadge";
|
||||
import { TableSkeleton } from "../components/common/TableSkeleton";
|
||||
@@ -244,6 +245,8 @@ export function PurchaseRequestsPage() {
|
||||
const [archivePoNo, setArchivePoNo] = useState("");
|
||||
const [archiveContractNo, setArchiveContractNo] = useState("");
|
||||
const [selectedAttachmentName, setSelectedAttachmentName] = useState("");
|
||||
const [submitPrecheckConfirm, setSubmitPrecheckConfirm] = useState<string | null>(null);
|
||||
const [erpRequeueConfirm, setErpRequeueConfirm] = useState<{ target: PurchaseRequestErpRequeueTarget; label: string } | null>(null);
|
||||
const attachmentInputRef = useRef<HTMLInputElement | null>(null);
|
||||
|
||||
const suppliersQuery = useQuery({
|
||||
@@ -742,7 +745,8 @@ export function PurchaseRequestsPage() {
|
||||
return;
|
||||
}
|
||||
|
||||
if ((precheck.decision === "warn" || precheck.decision === "manual_review") && !window.confirm(message)) {
|
||||
if (precheck.decision === "warn" || precheck.decision === "manual_review") {
|
||||
setSubmitPrecheckConfirm(message);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1736,7 +1740,7 @@ export function PurchaseRequestsPage() {
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
disabled={requeueErpMutation.isPending}
|
||||
onClick={() => requeueErpMutation.mutate({ target: action.target, label: meta.title })}
|
||||
onClick={() => setErpRequeueConfirm({ target: action.target, label: meta.title })}
|
||||
>
|
||||
{isSubmittingAction ? "处理中..." : action.label}
|
||||
</Button>
|
||||
@@ -1757,6 +1761,32 @@ export function PurchaseRequestsPage() {
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
<ActionConfirmDialog
|
||||
open={Boolean(submitPrecheckConfirm)}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) setSubmitPrecheckConfirm(null);
|
||||
}}
|
||||
kind="precheck"
|
||||
targetName={detail ? resolvePurchaseDisplayCode(detail) : "采购申请"}
|
||||
description={submitPrecheckConfirm}
|
||||
onConfirm={async () => {
|
||||
submitMutation.mutate();
|
||||
}}
|
||||
/>
|
||||
<ActionConfirmDialog
|
||||
open={Boolean(erpRequeueConfirm)}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) setErpRequeueConfirm(null);
|
||||
}}
|
||||
kind="retry"
|
||||
targetName={erpRequeueConfirm?.label}
|
||||
description="确认后会重新补发或重试 ERP 同步消息,并追加新的同步处理记录。"
|
||||
onConfirm={async () => {
|
||||
if (erpRequeueConfirm) {
|
||||
requeueErpMutation.mutate(erpRequeueConfirm);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</ListWorkspace>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ import { toast } from "sonner";
|
||||
import { downloadBlob } from "../lib/download";
|
||||
import { resolveReimbursementDisplayCode } from "../lib/reimbursementDisplay";
|
||||
import { Pagination } from "../components/common/Pagination";
|
||||
import { ActionConfirmDialog } from "../components/common/ActionConfirmDialog";
|
||||
import { StatusBadge } from "../components/common/StatusBadge";
|
||||
import { TableSkeleton } from "../components/common/TableSkeleton";
|
||||
import { FilterBar, ListWorkspace, PrimaryTable, StatStrip, WorkspaceHeader } from "../components/patterns/ListWorkspace";
|
||||
@@ -52,6 +53,7 @@ import { Textarea } from "../components/ui/textarea";
|
||||
const PAGE_SIZE = 20;
|
||||
|
||||
type BatchAction = "approve" | "reject";
|
||||
type ExportFormat = "csv" | "xlsx";
|
||||
|
||||
type BatchResultState = {
|
||||
action: BatchAction;
|
||||
@@ -104,6 +106,8 @@ export function ReimbursementsPage() {
|
||||
const [batchDialogAction, setBatchDialogAction] = useState<BatchAction | null>(null);
|
||||
const [batchComment, setBatchComment] = useState("");
|
||||
const [batchResult, setBatchResult] = useState<BatchResultState | null>(null);
|
||||
const [deleteTarget, setDeleteTarget] = useState<ReimbursementListItem | null>(null);
|
||||
const [exportConfirmFormat, setExportConfirmFormat] = useState<ExportFormat | null>(null);
|
||||
|
||||
const status = (searchParams.get("status") as ReimbursementStatus | null) ?? "draft";
|
||||
const page = Number(searchParams.get("page") ?? "1") || 1;
|
||||
@@ -216,15 +220,11 @@ export function ReimbursementsPage() {
|
||||
});
|
||||
|
||||
const handleDeleteDraft = (item: ReimbursementListItem) => {
|
||||
const displayCode = resolveReimbursementDisplayCode(item);
|
||||
if (!window.confirm(`确认删除 ${displayCode}?删除后无法恢复。`)) {
|
||||
return;
|
||||
}
|
||||
deleteMutation.mutate(item.id);
|
||||
setDeleteTarget(item);
|
||||
};
|
||||
|
||||
const exportMutation = useMutation({
|
||||
mutationFn: (format: "csv" | "xlsx") => exportReimbursements({ format, status, scope }),
|
||||
mutationFn: (format: ExportFormat) => exportReimbursements({ format, status, scope }),
|
||||
onSuccess: (response, format) => {
|
||||
downloadBlob(response.blob, response.filename ?? `reimbursements.${format}`);
|
||||
setExportError("");
|
||||
@@ -461,13 +461,13 @@ export function ReimbursementsPage() {
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem
|
||||
data-testid="reimbursements-export-csv"
|
||||
onSelect={() => exportMutation.mutate("csv")}
|
||||
onSelect={() => setExportConfirmFormat("csv")}
|
||||
>
|
||||
导出 CSV
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
data-testid="reimbursements-export-xlsx"
|
||||
onSelect={() => exportMutation.mutate("xlsx")}
|
||||
onSelect={() => setExportConfirmFormat("xlsx")}
|
||||
>
|
||||
导出 Excel
|
||||
</DropdownMenuItem>
|
||||
@@ -943,6 +943,33 @@ export function ReimbursementsPage() {
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<ActionConfirmDialog
|
||||
open={Boolean(deleteTarget)}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) setDeleteTarget(null);
|
||||
}}
|
||||
kind="delete"
|
||||
targetName={deleteTarget ? resolveReimbursementDisplayCode(deleteTarget) : undefined}
|
||||
description="删除后无法恢复,已提交或审批中的报销单不会受到影响。"
|
||||
onConfirm={async () => {
|
||||
if (deleteTarget) {
|
||||
deleteMutation.mutate(deleteTarget.id);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<ActionConfirmDialog
|
||||
open={Boolean(exportConfirmFormat)}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) setExportConfirmFormat(null);
|
||||
}}
|
||||
kind="sensitiveExport"
|
||||
targetName={exportConfirmFormat === "xlsx" ? "报销 Excel" : "报销 CSV"}
|
||||
onConfirm={async () => {
|
||||
if (exportConfirmFormat) {
|
||||
exportMutation.mutate(exportConfirmFormat);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</ListWorkspace>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ import { fetchCostCenters, fetchLegalEntities } from "../api/masterData";
|
||||
import { useAuth } from "../auth/AuthContext";
|
||||
import { hasPermission } from "../auth/permissions";
|
||||
import { CardSkeleton } from "../components/common/CardSkeleton";
|
||||
import { ActionConfirmDialog } from "../components/common/ActionConfirmDialog";
|
||||
import { EmptyStateCard } from "../components/common/EmptyStateCard";
|
||||
import { PageHeader } from "../components/common/PageHeader";
|
||||
import { RiskFlagsCard } from "../components/common/RiskFlagsCard";
|
||||
@@ -201,6 +202,7 @@ export function TravelRequestsPage() {
|
||||
const [costCenterId, setCostCenterId] = useState("");
|
||||
const [estimatedAmount, setEstimatedAmount] = useState("0.00");
|
||||
const [segments, setSegments] = useState<DraftSegment[]>([emptySegment()]);
|
||||
const [submitPrecheckConfirm, setSubmitPrecheckConfirm] = useState<string | null>(null);
|
||||
|
||||
const selectedQueryId = searchParams.get("id") ?? "";
|
||||
const selectedBatchId = searchParams.get("batchId") ?? "";
|
||||
@@ -351,7 +353,8 @@ export function TravelRequestsPage() {
|
||||
return;
|
||||
}
|
||||
|
||||
if ((precheck.decision === "warn" || precheck.decision === "manual_review") && !window.confirm(message)) {
|
||||
if (precheck.decision === "warn" || precheck.decision === "manual_review") {
|
||||
setSubmitPrecheckConfirm(message);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -510,8 +513,9 @@ export function TravelRequestsPage() {
|
||||
const totalPages = Math.max(1, Math.ceil((listQuery.data?.total ?? 0) / PAGE_SIZE));
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<PageHeader
|
||||
<>
|
||||
<div className="space-y-6">
|
||||
<PageHeader
|
||||
title="差旅管理"
|
||||
subtitle="差旅申请、对公支付轨道与批次对账在同一路径统一收口。"
|
||||
actions={
|
||||
@@ -918,7 +922,20 @@ export function TravelRequestsPage() {
|
||||
) : (
|
||||
<EmptyStateCard title="暂无可访问的差旅页签" description="当前账号缺少差旅申请与对账访问权限。" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<ActionConfirmDialog
|
||||
open={Boolean(submitPrecheckConfirm)}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) setSubmitPrecheckConfirm(null);
|
||||
}}
|
||||
kind="precheck"
|
||||
targetName={detail?.requestNo ?? "差旅申请"}
|
||||
description={submitPrecheckConfirm}
|
||||
onConfirm={async () => {
|
||||
submitMutation.mutate();
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
type WorkflowDefinitionEditorModel,
|
||||
} from "../api/workflowDefinitions";
|
||||
import { ListWorkspace, WorkspaceHeader } from "../components/patterns/ListWorkspace";
|
||||
import { ActionConfirmDialog } from "../components/common/ActionConfirmDialog";
|
||||
import { Button } from "../components/ui/button";
|
||||
import { Input } from "../components/ui/input";
|
||||
import { Label } from "../components/ui/label";
|
||||
@@ -29,6 +30,11 @@ export function WorkflowDefinitionsPage() {
|
||||
const [name, setName] = useState("");
|
||||
const [notes, setNotes] = useState("");
|
||||
const [steps, setSteps] = useState<DraftStep[]>([emptyStep()]);
|
||||
const [confirmDefinitionAction, setConfirmDefinitionAction] = useState<{
|
||||
action: "publish" | "archive";
|
||||
id: string;
|
||||
name: string;
|
||||
} | null>(null);
|
||||
|
||||
const listQuery = useQuery({ queryKey: ["workflow-definitions"], queryFn: () => fetchWorkflowDefinitions() });
|
||||
const items = listQuery.data?.items ?? [];
|
||||
@@ -114,8 +120,21 @@ export function WorkflowDefinitionsPage() {
|
||||
{selected.status !== "derived_readonly" ? (
|
||||
<div className="flex gap-2">
|
||||
<Button type="button" variant="secondary" onClick={() => saveMutation.mutate(selected.id)} disabled={selected.status !== "draft"}>保存</Button>
|
||||
<Button type="button" onClick={() => publishMutation.mutate(selected.id)} disabled={selected.status === "archived"}>发布</Button>
|
||||
<Button type="button" variant="secondary" onClick={() => archiveMutation.mutate(selected.id)} disabled={selected.status === "archived"}>归档</Button>
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => setConfirmDefinitionAction({ action: "publish", id: selected.id, name: selected.name })}
|
||||
disabled={selected.status === "archived"}
|
||||
>
|
||||
发布
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
onClick={() => setConfirmDefinitionAction({ action: "archive", id: selected.id, name: selected.name })}
|
||||
disabled={selected.status === "archived"}
|
||||
>
|
||||
归档
|
||||
</Button>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
@@ -163,6 +182,25 @@ export function WorkflowDefinitionsPage() {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<ActionConfirmDialog
|
||||
open={Boolean(confirmDefinitionAction)}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) setConfirmDefinitionAction(null);
|
||||
}}
|
||||
kind={confirmDefinitionAction?.action === "archive" ? "archive" : "submit"}
|
||||
targetName={confirmDefinitionAction?.name}
|
||||
title={confirmDefinitionAction?.action === "archive" ? "确认归档流程定义" : "确认发布流程定义"}
|
||||
description={
|
||||
confirmDefinitionAction?.action === "archive"
|
||||
? "归档后该流程定义不再用于新流程,历史流程不回写。"
|
||||
: "发布后该流程定义会参与后续新提交业务的审批解析,历史流程不回写。"
|
||||
}
|
||||
onConfirm={async () => {
|
||||
if (!confirmDefinitionAction) return;
|
||||
if (confirmDefinitionAction.action === "publish") publishMutation.mutate(confirmDefinitionAction.id);
|
||||
if (confirmDefinitionAction.action === "archive") archiveMutation.mutate(confirmDefinitionAction.id);
|
||||
}}
|
||||
/>
|
||||
</ListWorkspace>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import { useState } from "react";
|
||||
import type { UseMutationResult, UseQueryResult } from "@tanstack/react-query";
|
||||
import type { InvoicePoolDetail, InvoiceVerificationRecord } from "../../api/invoicePool";
|
||||
import { ConfirmDialog } from "../../components/common/ConfirmDialog";
|
||||
import { Button } from "../../components/ui/button";
|
||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "../../components/ui/dialog";
|
||||
import { Input } from "../../components/ui/input";
|
||||
import { Textarea } from "../../components/ui/textarea";
|
||||
import { formatInvoiceOcrErrorMessage } from "../../lib/invoiceOcr";
|
||||
|
||||
export type InvoiceDetailPanelProps = {
|
||||
@@ -138,6 +142,9 @@ export function InvoiceDetailPanel({
|
||||
confirmAction,
|
||||
setConfirmAction,
|
||||
}: InvoiceDetailPanelProps) {
|
||||
const [duplicateOverrideTarget, setDuplicateOverrideTarget] = useState<string | null>(null);
|
||||
const [duplicateOverrideRefFormNumber, setDuplicateOverrideRefFormNumber] = useState("");
|
||||
const [duplicateOverrideReason, setDuplicateOverrideReason] = useState("");
|
||||
const invoiceNo = detail
|
||||
? [detail.invoiceCode, detail.invoiceNumber].filter(Boolean).join("-") || detail.originalName || "待识别"
|
||||
: "";
|
||||
@@ -148,6 +155,17 @@ export function InvoiceDetailPanel({
|
||||
: detail?.linkedItemId
|
||||
? "已绑定明细"
|
||||
: "未绑定";
|
||||
const canSubmitDuplicateOverride = Boolean(
|
||||
duplicateOverrideTarget &&
|
||||
duplicateOverrideRefFormNumber.trim() &&
|
||||
duplicateOverrideReason.trim() &&
|
||||
!overrideDuplicateMutation.isPending,
|
||||
);
|
||||
const resetDuplicateOverride = () => {
|
||||
setDuplicateOverrideTarget(null);
|
||||
setDuplicateOverrideRefFormNumber("");
|
||||
setDuplicateOverrideReason("");
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -291,16 +309,9 @@ export function InvoiceDetailPanel({
|
||||
type="button"
|
||||
disabled={overrideDuplicateMutation.isPending}
|
||||
onClick={() => {
|
||||
if (!window.confirm("确认执行判重例外?该操作将写入审计并影响判重口径。")) return;
|
||||
const refFormNumber = window.prompt("请输入参考单号(必填)");
|
||||
if (!refFormNumber?.trim()) return;
|
||||
const reason = window.prompt("请输入例外原因(必填)");
|
||||
if (!reason?.trim()) return;
|
||||
overrideDuplicateMutation.mutate({
|
||||
invoiceId: detail.id,
|
||||
refFormNumber: refFormNumber.trim(),
|
||||
reason: reason.trim(),
|
||||
});
|
||||
setDuplicateOverrideTarget(detail.id);
|
||||
setDuplicateOverrideRefFormNumber("");
|
||||
setDuplicateOverrideReason("");
|
||||
}}
|
||||
>
|
||||
判重例外
|
||||
@@ -515,6 +526,76 @@ export function InvoiceDetailPanel({
|
||||
}}
|
||||
onOpenChange={(open) => { if (!open) setConfirmAction(null); }}
|
||||
/>
|
||||
<Dialog
|
||||
open={Boolean(duplicateOverrideTarget)}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) resetDuplicateOverride();
|
||||
}}
|
||||
>
|
||||
<DialogContent className="max-w-md" data-testid="action-confirm-dialog">
|
||||
<DialogHeader>
|
||||
<DialogTitle>确认判重例外</DialogTitle>
|
||||
<DialogDescription>
|
||||
确认后将写入审计记录,并影响该票据后续判重口径。请填写参考单号和例外原因。
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-zinc-900" htmlFor="invoice-duplicate-ref-form-number">
|
||||
参考单号
|
||||
</label>
|
||||
<Input
|
||||
id="invoice-duplicate-ref-form-number"
|
||||
data-testid="invoice-duplicate-ref-form-number"
|
||||
value={duplicateOverrideRefFormNumber}
|
||||
onChange={(event) => setDuplicateOverrideRefFormNumber(event.target.value)}
|
||||
placeholder="例如:BX-20260514-0016"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-zinc-900" htmlFor="invoice-duplicate-override-reason">
|
||||
例外原因
|
||||
</label>
|
||||
<Textarea
|
||||
id="invoice-duplicate-override-reason"
|
||||
data-testid="invoice-duplicate-override-reason"
|
||||
value={duplicateOverrideReason}
|
||||
onChange={(event) => setDuplicateOverrideReason(event.target.value)}
|
||||
placeholder="说明为什么该票据可以作为例外处理"
|
||||
rows={3}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
data-testid="action-confirm-cancel"
|
||||
disabled={overrideDuplicateMutation.isPending}
|
||||
onClick={resetDuplicateOverride}
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="destructive"
|
||||
data-testid="action-confirm-submit"
|
||||
disabled={!canSubmitDuplicateOverride}
|
||||
onClick={() => {
|
||||
if (!duplicateOverrideTarget) return;
|
||||
overrideDuplicateMutation.mutate({
|
||||
invoiceId: duplicateOverrideTarget,
|
||||
refFormNumber: duplicateOverrideRefFormNumber.trim(),
|
||||
reason: duplicateOverrideReason.trim(),
|
||||
});
|
||||
resetDuplicateOverride();
|
||||
}}
|
||||
>
|
||||
{overrideDuplicateMutation.isPending ? "处理中..." : "确认例外"}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
在新工单中引用
屏蔽一个用户