R7E-16E/R7E-16F 高风险操作确认统一
这个提交包含在:
@@ -0,0 +1,52 @@
|
||||
import type { ReactNode } from "react";
|
||||
import { getActionConfirmCopy, type ActionConfirmKind } from "../../lib/actionConfirm";
|
||||
import { ConfirmDialog } from "./ConfirmDialog";
|
||||
|
||||
type ActionConfirmDialogProps = {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
kind: ActionConfirmKind;
|
||||
targetName?: string;
|
||||
count?: number;
|
||||
title?: string;
|
||||
description?: ReactNode;
|
||||
confirmLabel?: string;
|
||||
variant?: "default" | "destructive";
|
||||
testId?: string;
|
||||
confirmTestId?: string;
|
||||
cancelTestId?: string;
|
||||
onConfirm: () => void | Promise<void>;
|
||||
};
|
||||
|
||||
export function ActionConfirmDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
kind,
|
||||
targetName,
|
||||
count,
|
||||
title,
|
||||
description,
|
||||
confirmLabel,
|
||||
variant,
|
||||
testId,
|
||||
confirmTestId,
|
||||
cancelTestId,
|
||||
onConfirm,
|
||||
}: ActionConfirmDialogProps) {
|
||||
const copy = getActionConfirmCopy(kind, { targetName, count, detail: description });
|
||||
|
||||
return (
|
||||
<ConfirmDialog
|
||||
open={open}
|
||||
onOpenChange={onOpenChange}
|
||||
title={title ?? copy.title}
|
||||
description={description ?? copy.description}
|
||||
confirmLabel={confirmLabel ?? copy.confirmLabel}
|
||||
variant={variant ?? copy.variant}
|
||||
testId={testId}
|
||||
confirmTestId={confirmTestId}
|
||||
cancelTestId={cancelTestId}
|
||||
onConfirm={onConfirm}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -31,9 +31,9 @@ export function ConfirmDialog({
|
||||
confirmLabel = "确认",
|
||||
cancelLabel = "取消",
|
||||
variant = "default",
|
||||
testId,
|
||||
confirmTestId,
|
||||
cancelTestId,
|
||||
testId = "action-confirm-dialog",
|
||||
confirmTestId = "action-confirm-submit",
|
||||
cancelTestId = "action-confirm-cancel",
|
||||
onConfirm,
|
||||
}: ConfirmDialogProps) {
|
||||
const [pending, setPending] = useState(false);
|
||||
|
||||
@@ -25,8 +25,10 @@ import { EmptyStateCard } from '../common/EmptyStateCard';
|
||||
import { Pagination } from '../common/Pagination';
|
||||
import { StatusBadge } from '../common/StatusBadge';
|
||||
import { Button } from '../ui/button';
|
||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '../ui/dialog';
|
||||
import { Input } from '../ui/input';
|
||||
import { Label } from '../ui/label';
|
||||
import { Textarea } from '../ui/textarea';
|
||||
|
||||
const PAGE_SIZE = 10;
|
||||
|
||||
@@ -68,6 +70,8 @@ export function TravelReconciliationTab({ selectedBatchId, onSelectBatch }: Trav
|
||||
const [costCenterFilter, setCostCenterFilter] = useState('');
|
||||
const [periodStartFilter, setPeriodStartFilter] = useState('');
|
||||
const [periodEndFilter, setPeriodEndFilter] = useState('');
|
||||
const [confirmItemTarget, setConfirmItemTarget] = useState<TravelReconciliationItem | null>(null);
|
||||
const [confirmItemNote, setConfirmItemNote] = useState('');
|
||||
const [page, setPage] = useState(1);
|
||||
const [actionItemId, setActionItemId] = useState<string | null>(null);
|
||||
|
||||
@@ -248,11 +252,8 @@ export function TravelReconciliationTab({ selectedBatchId, onSelectBatch }: Trav
|
||||
});
|
||||
|
||||
function handleConfirm(item: TravelReconciliationItem) {
|
||||
const note = window.prompt('确认备注(可选)', item.confirmNote ?? '');
|
||||
if (note === null) {
|
||||
return;
|
||||
}
|
||||
confirmMutation.mutate({ item, note: note.trim() || null });
|
||||
setConfirmItemTarget(item);
|
||||
setConfirmItemNote(item.confirmNote ?? '');
|
||||
}
|
||||
|
||||
const detail = detailQuery.data ?? null;
|
||||
@@ -260,6 +261,7 @@ export function TravelReconciliationTab({ selectedBatchId, onSelectBatch }: Trav
|
||||
const totalPages = Math.max(1, Math.ceil((listQuery.data?.total ?? 0) / PAGE_SIZE));
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="grid gap-6 xl:grid-cols-[360px_minmax(0,1fr)]">
|
||||
<section className="space-y-6">
|
||||
{canCreateBatch ? (
|
||||
@@ -520,5 +522,64 @@ export function TravelReconciliationTab({ selectedBatchId, onSelectBatch }: Trav
|
||||
) : null}
|
||||
</section>
|
||||
</div>
|
||||
<Dialog
|
||||
open={Boolean(confirmItemTarget)}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
setConfirmItemTarget(null);
|
||||
setConfirmItemNote('');
|
||||
}
|
||||
}}
|
||||
>
|
||||
<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="travel-reconciliation-confirm-note">
|
||||
确认备注
|
||||
</label>
|
||||
<Textarea
|
||||
id="travel-reconciliation-confirm-note"
|
||||
data-testid="travel-reconciliation-confirm-note"
|
||||
value={confirmItemNote}
|
||||
onChange={(event) => setConfirmItemNote(event.target.value)}
|
||||
placeholder="可补充异常确认说明"
|
||||
rows={3}
|
||||
/>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
data-testid="action-confirm-cancel"
|
||||
disabled={confirmMutation.isPending}
|
||||
onClick={() => {
|
||||
setConfirmItemTarget(null);
|
||||
setConfirmItemNote('');
|
||||
}}
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
data-testid="action-confirm-submit"
|
||||
disabled={!confirmItemTarget || confirmMutation.isPending}
|
||||
onClick={() => {
|
||||
if (!confirmItemTarget) return;
|
||||
confirmMutation.mutate({ item: confirmItemTarget, note: confirmItemNote.trim() || null });
|
||||
setConfirmItemTarget(null);
|
||||
setConfirmItemNote('');
|
||||
}}
|
||||
>
|
||||
{confirmMutation.isPending ? '处理中...' : '确认异常'}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
在新工单中引用
屏蔽一个用户