import { useEffect, useMemo, useState } from "react"; import { useNavigate } from "react-router-dom"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { toast } from "sonner"; import { fetchAssets } from "../api/assets"; import { mapApiErrorMessage } from "../api/errorMessages"; import { completeInventoryCheck, createInventoryCheck, fetchInventoryChecks, recordInventoryItems, startInventoryCheck, type InventoryCheckResult, type InventoryCheckScopeType, } from "../api/inventoryChecks"; import { fetchCostCenters, fetchLegalEntities } from "../api/masterData"; import { isApiError } from "../api/types"; import { ActionConfirmDialog } from "../components/common/ActionConfirmDialog"; import { PageHeader } from "../components/common/PageHeader"; import { TableSkeleton } from "../components/common/TableSkeleton"; import { PageScaffold } from "../components/patterns/PageScaffold"; import { Button } from "../components/ui/button"; import { Input } from "../components/ui/input"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "../components/ui/table"; function resolveError(error: unknown) { if (isApiError(error)) { return `${error.code}: ${mapApiErrorMessage(error.code, error.details, error.message)}`; } if (error instanceof Error) { return error.message; } return "请求失败,请稍后重试。"; } function scopeLabel(scopeType: InventoryCheckScopeType) { switch (scopeType) { case "all": return "全量盘点"; case "legal_entity": return "公司盘点"; case "cost_center": return "成本中心盘点"; case "location": return "位置盘点"; default: return scopeType; } } function resultLabel(result: InventoryCheckResult) { switch (result) { case "matched": return "一致"; case "missing": return "缺失"; case "extra": return "盘盈"; case "damaged": return "损坏"; default: return result; } } function statusLabel(status: string) { switch (status) { case "draft": return "草稿"; case "counting": return "盘点中"; case "completed": return "已完成"; default: return status; } } export function InventoryChecksPage() { const navigate = useNavigate(); const queryClient = useQueryClient(); const [scopeType, setScopeType] = useState("all"); const [scopeRefId, setScopeRefId] = useState(""); const [selectedCheckId, setSelectedCheckId] = useState(""); const [recordAssetId, setRecordAssetId] = useState(""); const [recordResult, setRecordResult] = useState("matched"); const [recordNote, setRecordNote] = useState(""); const [confirmCompleteCheck, setConfirmCompleteCheck] = useState(false); const inventoryChecksQuery = useQuery({ queryKey: ["inventory-checks"], queryFn: () => fetchInventoryChecks(), }); const legalEntitiesQuery = useQuery({ queryKey: ["inventory-checks", "legal-entities"], queryFn: () => fetchLegalEntities({ enabled: true }), }); const costCentersQuery = useQuery({ queryKey: ["inventory-checks", "cost-centers"], queryFn: () => fetchCostCenters({ enabled: true }), }); const assetsQuery = useQuery({ queryKey: ["inventory-checks", "assets"], queryFn: () => fetchAssets({ mine: false }), }); useEffect(() => { if (!selectedCheckId) { return; } if (!inventoryChecksQuery.data?.items?.some((item) => item.id === selectedCheckId)) { setSelectedCheckId(""); } }, [inventoryChecksQuery.data?.items, selectedCheckId]); const selectedCheck = useMemo( () => (inventoryChecksQuery.data?.items ?? []).find((item) => item.id === selectedCheckId) ?? null, [inventoryChecksQuery.data?.items, selectedCheckId], ); const inventoryStats = useMemo(() => { const items = inventoryChecksQuery.data?.items ?? []; return items.reduce( (acc, item) => { acc.total += 1; if (item.status === "draft") acc.draft += 1; if (item.status === "counting") acc.counting += 1; if (item.status === "completed") acc.completed += 1; return acc; }, { total: 0, draft: 0, counting: 0, completed: 0 }, ); }, [inventoryChecksQuery.data?.items]); const scopedAssets = useMemo(() => { const items = assetsQuery.data?.items ?? []; const scope = selectedCheck ?? { scopeType, scopeRefId: scopeRefId || null }; if (scope.scopeType === "all") { return items; } if (scope.scopeType === "legal_entity") { return items.filter((item) => item.legalEntityId === scope.scopeRefId); } if (scope.scopeType === "cost_center") { return items.filter((item) => item.costCenterId === scope.scopeRefId); } return items.filter((item) => (item.location ?? "") === (scope.scopeRefId ?? "")); }, [assetsQuery.data?.items, scopeRefId, scopeType, selectedCheck]); const createCheckMutation = useMutation({ mutationFn: async () => { const payload = { scopeType, scopeRefId: scopeType === "all" ? null : scopeRefId || null, }; return createInventoryCheck(payload); }, onSuccess: async (data) => { await queryClient.invalidateQueries({ queryKey: ["inventory-checks"] }); setSelectedCheckId(data.inventoryCheck.id); toast.success("盘点任务已创建"); }, onError: (error) => { toast.error(resolveError(error)); }, }); const startCheckMutation = useMutation({ mutationFn: async () => { if (!selectedCheck) { throw new Error("请先选择盘点任务"); } return startInventoryCheck(selectedCheck.id); }, onSuccess: async () => { await queryClient.invalidateQueries({ queryKey: ["inventory-checks"] }); toast.success("盘点任务已启动"); }, onError: (error) => { toast.error(resolveError(error)); }, }); const recordItemMutation = useMutation({ mutationFn: async () => { if (!selectedCheck) { throw new Error("请先选择盘点任务"); } if (!recordAssetId) { throw new Error("请选择资产卡片"); } return recordInventoryItems(selectedCheck.id, { items: [ { assetId: recordAssetId, result: recordResult, note: recordNote.trim() || null, }, ], }); }, onSuccess: async () => { await queryClient.invalidateQueries({ queryKey: ["inventory-checks"] }); setRecordAssetId(""); setRecordNote(""); toast.success("盘点结果已记录"); }, onError: (error) => { toast.error(resolveError(error)); }, }); const completeCheckMutation = useMutation({ mutationFn: async () => { if (!selectedCheck) { throw new Error("请先选择盘点任务"); } return completeInventoryCheck(selectedCheck.id); }, onSuccess: async () => { await queryClient.invalidateQueries({ queryKey: ["inventory-checks"] }); toast.success("盘点任务已完成"); }, onError: (error) => { toast.error(resolveError(error)); }, }); const scopeOptions = useMemo(() => { if (scopeType === "legal_entity") { return (legalEntitiesQuery.data?.items ?? []).map((item) => ({ id: item.id, label: item.name })); } if (scopeType === "cost_center") { return (costCentersQuery.data?.items ?? []).map((item) => ({ id: item.id, label: item.name })); } return []; }, [costCentersQuery.data?.items, legalEntitiesQuery.data?.items, scopeType]); return ( navigate(selectedCheckId ? `/mobile/assets/scan?inventoryCheckId=${selectedCheckId}` : "/mobile/assets/scan")} > 移动扫码盘点 } />
{[ ["全部", inventoryStats.total], ["草稿", inventoryStats.draft], ["盘点中", inventoryStats.counting], ["已完成", inventoryStats.completed], ].map(([label, value]) => ( {label} {value} ))}

新建盘点任务

{scopeType === "location" ? (
setScopeRefId(event.target.value)} />
) : null} {scopeType !== "all" && scopeType !== "location" ? (
) : null}
单号范围状态 {(inventoryChecksQuery.data?.items ?? []).map((item) => ( setSelectedCheckId(item.id)} > {item.checkNo} {scopeLabel(item.scopeType)} {statusLabel(item.status)} ))} {!inventoryChecksQuery.isLoading && !(inventoryChecksQuery.data?.items ?? []).length ? ( 暂无盘点任务 ) : null}
{selectedCheck ? (
{inventoryChecksQuery.isLoading ? : null} {inventoryChecksQuery.isError ? (

{resolveError(inventoryChecksQuery.error)}

) : null} {selectedCheck ? ( <>

{selectedCheck.checkNo}

{scopeLabel(selectedCheck.scopeType)} · {selectedCheck.scopeRefId ?? "全量"}

{statusLabel(selectedCheck.status)} {selectedCheck.status === "draft" ? ( ) : null} {selectedCheck.status === "counting" ? ( ) : null}
{selectedCheck.status === "counting" ? (

记录盘点结果

setRecordNote(event.target.value)} />
) : null}
资产结果备注 {selectedCheck.items.length ? ( selectedCheck.items.map((item) => (
{item.asset?.assetNo ?? item.assetId}
{item.asset?.name ?? "未知资产"}
{resultLabel(item.result)} {item.note ?? "-"}
)) ) : ( 暂无盘点结果 )}
) : null}
) : null}
completeCheckMutation.mutate()} />
); }