完成五人团队正式内用最小收口(07-18-small-team-internal-readiness)
这个提交包含在:
@@ -35,8 +35,10 @@ import { RiskFlagsCard } from "../components/common/RiskFlagsCard";
|
||||
import { AiSuggestionCard } from "../components/ai/AiSuggestionCard";
|
||||
import { ActionConfirmDialog } from "../components/common/ActionConfirmDialog";
|
||||
import { AttachmentGovernanceBadges } from "../components/common/AttachmentGovernanceBadges";
|
||||
import { AttachmentDeleteConfirmDialog } from "../components/common/AttachmentDeleteConfirmDialog";
|
||||
import { useAttachmentDownloadReasonDialog } from "../components/common/useAttachmentDownloadReasonDialog";
|
||||
import { DateTextInput } from "../components/common/DateTextInput";
|
||||
import { useUnsavedChangesGuard } from "../components/common/UnsavedChangesGuard";
|
||||
import { Pagination } from "../components/common/Pagination";
|
||||
import { StatusBadge } from "../components/common/StatusBadge";
|
||||
import { TableSkeleton } from "../components/common/TableSkeleton";
|
||||
@@ -77,6 +79,21 @@ type DraftItem = {
|
||||
isAssetCandidate: boolean;
|
||||
};
|
||||
|
||||
function purchaseDraftSignature(value: {
|
||||
title: string;
|
||||
reason: string;
|
||||
currencyCode: string;
|
||||
legalEntityId: string;
|
||||
costCenterId: string;
|
||||
supplierId: string;
|
||||
documentCategoryId: string;
|
||||
projectId: string;
|
||||
expectedDeliveryDate: string;
|
||||
items: DraftItem[];
|
||||
}) {
|
||||
return JSON.stringify(value);
|
||||
}
|
||||
|
||||
function resolveError(error: unknown) {
|
||||
if (isApiError(error)) {
|
||||
return mapApiErrorMessage(error.code, error.details, error.message);
|
||||
@@ -318,9 +335,24 @@ export function PurchaseRequestsPage() {
|
||||
const [projectId, setProjectId] = useState("");
|
||||
const [expectedDeliveryDate, setExpectedDeliveryDate] = useState("");
|
||||
const [items, setItems] = useState<DraftItem[]>([]);
|
||||
const [savedDraftSignature, setSavedDraftSignature] = useState(() =>
|
||||
purchaseDraftSignature({
|
||||
title: "",
|
||||
reason: "",
|
||||
currencyCode: "CNY",
|
||||
legalEntityId: "",
|
||||
costCenterId: "",
|
||||
supplierId: "",
|
||||
documentCategoryId: "",
|
||||
projectId: "",
|
||||
expectedDeliveryDate: "",
|
||||
items: [],
|
||||
}),
|
||||
);
|
||||
const [archivePoNo, setArchivePoNo] = useState("");
|
||||
const [archiveContractNo, setArchiveContractNo] = useState("");
|
||||
const [selectedAttachmentName, setSelectedAttachmentName] = useState("");
|
||||
const [deleteAttachmentTarget, setDeleteAttachmentTarget] = useState<{ id: string; originalName: string } | null>(null);
|
||||
const [submitPrecheckConfirm, setSubmitPrecheckConfirm] = useState<string | null>(null);
|
||||
const attachmentInputRef = useRef<HTMLInputElement | null>(null);
|
||||
|
||||
@@ -403,17 +435,17 @@ export function PurchaseRequestsPage() {
|
||||
if (!detailQuery.data || detailQuery.data.id !== selectedId || hydratedDetailId === detailQuery.data.id) {
|
||||
return;
|
||||
}
|
||||
setTitle(detailQuery.data.title || "");
|
||||
setReason(detailQuery.data.reason || "");
|
||||
setCurrencyCode(detailQuery.data.currencyCode || "CNY");
|
||||
setLegalEntityId(detailQuery.data.legalEntityId || "");
|
||||
setCostCenterId(detailQuery.data.costCenterId || "");
|
||||
setSupplierId(detailQuery.data.supplierId || "");
|
||||
setDocumentCategoryId(detailQuery.data.documentCategoryId || "");
|
||||
setProjectId(detailQuery.data.projectId || "");
|
||||
setExpectedDeliveryDate(detailQuery.data.expectedDeliveryDate || "");
|
||||
setItems(
|
||||
detailQuery.data.items.map((item) => ({
|
||||
const nextDraft = {
|
||||
title: detailQuery.data.title || "",
|
||||
reason: detailQuery.data.reason || "",
|
||||
currencyCode: detailQuery.data.currencyCode || "CNY",
|
||||
legalEntityId: detailQuery.data.legalEntityId || "",
|
||||
costCenterId: detailQuery.data.costCenterId || "",
|
||||
supplierId: detailQuery.data.supplierId || "",
|
||||
documentCategoryId: detailQuery.data.documentCategoryId || "",
|
||||
projectId: detailQuery.data.projectId || "",
|
||||
expectedDeliveryDate: detailQuery.data.expectedDeliveryDate || "",
|
||||
items: detailQuery.data.items.map((item) => ({
|
||||
id: item.id,
|
||||
description: item.description,
|
||||
specification: item.specification || "",
|
||||
@@ -421,7 +453,18 @@ export function PurchaseRequestsPage() {
|
||||
unitPrice: item.unitPrice,
|
||||
isAssetCandidate: item.isAssetCandidate,
|
||||
})),
|
||||
);
|
||||
};
|
||||
setTitle(nextDraft.title);
|
||||
setReason(nextDraft.reason);
|
||||
setCurrencyCode(nextDraft.currencyCode);
|
||||
setLegalEntityId(nextDraft.legalEntityId);
|
||||
setCostCenterId(nextDraft.costCenterId);
|
||||
setSupplierId(nextDraft.supplierId);
|
||||
setDocumentCategoryId(nextDraft.documentCategoryId);
|
||||
setProjectId(nextDraft.projectId);
|
||||
setExpectedDeliveryDate(nextDraft.expectedDeliveryDate);
|
||||
setItems(nextDraft.items);
|
||||
setSavedDraftSignature(purchaseDraftSignature(nextDraft));
|
||||
setArchivePoNo(detailQuery.data.archiveOrder?.poNo || "");
|
||||
setArchiveContractNo(detailQuery.data.archiveOrder?.contractNo || "");
|
||||
setFormError("");
|
||||
@@ -576,6 +619,7 @@ export function PurchaseRequestsPage() {
|
||||
queryClient.invalidateQueries({ queryKey: ["purchase-requests", "detail", selectedId] }),
|
||||
]);
|
||||
setFormError("");
|
||||
setSavedDraftSignature(currentDraftSignature);
|
||||
toast.success("采购草稿已保存");
|
||||
},
|
||||
onError: (error) => {
|
||||
@@ -716,14 +760,15 @@ export function PurchaseRequestsPage() {
|
||||
});
|
||||
|
||||
const deleteAttachmentMutation = useMutation({
|
||||
mutationFn: async (attachmentId: string) => {
|
||||
mutationFn: async ({ attachmentId, reason }: { attachmentId: string; reason: string }) => {
|
||||
if (!selectedId) {
|
||||
throw new Error("请先选择采购单");
|
||||
}
|
||||
return deletePurchaseAttachment(selectedId, attachmentId);
|
||||
return deletePurchaseAttachment(selectedId, attachmentId, reason);
|
||||
},
|
||||
onSuccess: async () => {
|
||||
await queryClient.invalidateQueries({ queryKey: ["purchase-requests", "detail", selectedId] });
|
||||
setDeleteAttachmentTarget(null);
|
||||
toast.success("附件已删除");
|
||||
},
|
||||
onError: (error) => {
|
||||
@@ -772,6 +817,7 @@ export function PurchaseRequestsPage() {
|
||||
setFormError("");
|
||||
validateBeforeSubmit();
|
||||
await updatePurchaseRequest(selectedId, buildPayload());
|
||||
setSavedDraftSignature(currentDraftSignature);
|
||||
const precheck = await precheckPurchaseRequest(selectedId);
|
||||
const message = buildPrecheckDialogMessage(precheck);
|
||||
|
||||
@@ -796,6 +842,37 @@ export function PurchaseRequestsPage() {
|
||||
|
||||
const showArchiveSection = Boolean(detail && (detail.status === "approved" || archiveOrder));
|
||||
const canEditDraft = Boolean(detail?.permissions.canEditDraft);
|
||||
const currentDraftSignature = useMemo(
|
||||
() =>
|
||||
purchaseDraftSignature({
|
||||
title,
|
||||
reason,
|
||||
currencyCode,
|
||||
legalEntityId,
|
||||
costCenterId,
|
||||
supplierId,
|
||||
documentCategoryId,
|
||||
projectId,
|
||||
expectedDeliveryDate,
|
||||
items,
|
||||
}),
|
||||
[
|
||||
costCenterId,
|
||||
currencyCode,
|
||||
documentCategoryId,
|
||||
expectedDeliveryDate,
|
||||
items,
|
||||
legalEntityId,
|
||||
projectId,
|
||||
reason,
|
||||
supplierId,
|
||||
title,
|
||||
],
|
||||
);
|
||||
const runGuarded = useUnsavedChangesGuard(
|
||||
Boolean(selectedId) && canEditDraft && currentDraftSignature !== savedDraftSignature,
|
||||
"采购表单",
|
||||
);
|
||||
const canSubmit = Boolean(detail?.permissions.canSubmit);
|
||||
const canWithdraw = Boolean(detail?.permissions.canWithdraw);
|
||||
const canArchiveOrder = Boolean(detail?.permissions.canArchiveOrder && canManage);
|
||||
@@ -813,15 +890,19 @@ export function PurchaseRequestsPage() {
|
||||
const detailNextStepText = detail ? resolvePurchaseNextStep(detail) : "";
|
||||
const isPurchaseDetailMode = Boolean(selectedId);
|
||||
const openPurchaseDetail = (id: string) => {
|
||||
setSelectedId(id);
|
||||
setHydratedDetailId("");
|
||||
runGuarded(() => {
|
||||
setSelectedId(id);
|
||||
setHydratedDetailId("");
|
||||
});
|
||||
};
|
||||
const returnToPurchaseList = () => {
|
||||
setSelectedId("");
|
||||
setHydratedDetailId("");
|
||||
const next = new URLSearchParams(searchParams);
|
||||
next.delete("id");
|
||||
setSearchParams(next, { replace: true });
|
||||
runGuarded(() => {
|
||||
setSelectedId("");
|
||||
setHydratedDetailId("");
|
||||
const next = new URLSearchParams(searchParams);
|
||||
next.delete("id");
|
||||
setSearchParams(next, { replace: true });
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -1670,7 +1751,7 @@ export function PurchaseRequestsPage() {
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
disabled={!canEditDraft || deleteAttachmentMutation.isPending}
|
||||
onClick={() => deleteAttachmentMutation.mutate(attachment.id)}
|
||||
onClick={() => setDeleteAttachmentTarget({ id: attachment.id, originalName: attachment.originalName })}
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
@@ -1720,7 +1801,7 @@ export function PurchaseRequestsPage() {
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
disabled={!canEditDraft || deleteAttachmentMutation.isPending}
|
||||
onClick={() => deleteAttachmentMutation.mutate(attachment.id)}
|
||||
onClick={() => setDeleteAttachmentTarget({ id: attachment.id, originalName: attachment.originalName })}
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
@@ -1877,6 +1958,18 @@ export function PurchaseRequestsPage() {
|
||||
submitMutation.mutate();
|
||||
}}
|
||||
/>
|
||||
<AttachmentDeleteConfirmDialog
|
||||
open={Boolean(deleteAttachmentTarget)}
|
||||
targetName={deleteAttachmentTarget?.originalName}
|
||||
pending={deleteAttachmentMutation.isPending}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) setDeleteAttachmentTarget(null);
|
||||
}}
|
||||
onConfirm={(deleteReason) => {
|
||||
if (!deleteAttachmentTarget) return;
|
||||
deleteAttachmentMutation.mutate({ attachmentId: deleteAttachmentTarget.id, reason: deleteReason });
|
||||
}}
|
||||
/>
|
||||
</ListWorkspace>
|
||||
);
|
||||
}
|
||||
|
||||
在新工单中引用
屏蔽一个用户