import { useMemo, useState } from "react"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { toast } from "sonner"; import { mapApiErrorMessage } from "../api/errorMessages"; import { archiveCostCenter, archiveDocumentCategory, archiveLegalEntity, createCostCenter, createDocumentCategory, createLegalEntity, fetchCostCenters, fetchDocumentCategories, fetchLegalEntities, type CostCenterItem, type DocumentCategoryEntityType, type DocumentResetPeriod, } from "../api/masterData"; import { isApiError } from "../api/types"; import { useAuth } from "../auth/AuthContext"; import { hasAnyPermission, hasPermission } from "../auth/permissions"; import { ConfirmDialog } from "../components/common/ConfirmDialog"; import { HolidayCalendarTab } from "../components/master-data/HolidayCalendarTab"; import { SupplierTab } from "../components/master-data/SupplierTab"; import { StatusBadge } from "../components/common/StatusBadge"; import { TableSkeleton } from "../components/common/TableSkeleton"; import { FilterBar, ListWorkspace, PrimaryTable, WorkspaceHeader } from "../components/patterns/ListWorkspace"; import { Button } from "../components/ui/button"; import { Input } from "../components/ui/input"; import { Label } from "../components/ui/label"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "../components/ui/table"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "../components/ui/tabs"; const DOCUMENT_ENTITY_TYPE_OPTIONS: Array<{ value: DocumentCategoryEntityType; label: string }> = [ { value: "reimbursement", label: "报销" }, { value: "leave", label: "请假" }, { value: "purchase_request", label: "采购申请" }, { value: "asset_card", label: "资产卡片" }, { value: "general", label: "通用" }, ]; const RESET_PERIOD_OPTIONS: Array<{ value: DocumentResetPeriod; label: string }> = [ { value: "none", label: "不重置" }, { value: "daily", label: "按日重置" }, { value: "monthly", label: "按月重置" }, { value: "yearly", label: "按年重置" }, ]; 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 toEnabledFilter(value: "" | "true" | "false") { if (value === "") { return undefined; } return value === "true"; } function renderEnabledBadge(item: { archivedAt: string | null; enabled: boolean }) { if (item.archivedAt) { return ; } if (item.enabled) { return ; } return ; } export function MasterDataPage() { const queryClient = useQueryClient(); const { permissions } = useAuth(); const canView = hasAnyPermission(permissions, ["masterdata.view", "masterdata.manage"]); const canManage = hasPermission(permissions, "masterdata.manage"); const [tab, setTab] = useState<"legal-entities" | "cost-centers" | "document-categories" | "holiday-calendars" | "suppliers">( "legal-entities", ); const [includeArchived, setIncludeArchived] = useState(false); const [error, setError] = useState(""); const [confirmArchive, setConfirmArchive] = useState< | null | { kind: "legal-entity" | "cost-center" | "document-category"; id: string; label: string; } >(null); const [legalEntityEnabledFilter, setLegalEntityEnabledFilter] = useState<"" | "true" | "false">(""); const [costCenterEnabledFilter, setCostCenterEnabledFilter] = useState<"" | "true" | "false">(""); const [costCenterLegalEntityFilter, setCostCenterLegalEntityFilter] = useState(""); const [documentCategoryEnabledFilter, setDocumentCategoryEnabledFilter] = useState<"" | "true" | "false">(""); const [documentCategoryEntityTypeFilter, setDocumentCategoryEntityTypeFilter] = useState< "" | DocumentCategoryEntityType >(""); const [legalEntityCode, setLegalEntityCode] = useState(""); const [legalEntityName, setLegalEntityName] = useState(""); const [legalEntityShortName, setLegalEntityShortName] = useState(""); const [legalEntityCurrency, setLegalEntityCurrency] = useState("CNY"); const [legalEntityTaxNo, setLegalEntityTaxNo] = useState(""); const [costCenterLegalEntityId, setCostCenterLegalEntityId] = useState(""); const [costCenterCode, setCostCenterCode] = useState(""); const [costCenterName, setCostCenterName] = useState(""); const [costCenterParentId, setCostCenterParentId] = useState(""); const [documentCategoryCode, setDocumentCategoryCode] = useState(""); const [documentCategoryName, setDocumentCategoryName] = useState(""); const [documentCategoryEntityType, setDocumentCategoryEntityType] = useState("reimbursement"); const [documentCategorySequencePrefix, setDocumentCategorySequencePrefix] = useState("BX"); const [documentCategoryNumberLength, setDocumentCategoryNumberLength] = useState("4"); const [documentCategoryResetPeriod, setDocumentCategoryResetPeriod] = useState("yearly"); const [documentCategoryWorkflowKey, setDocumentCategoryWorkflowKey] = useState(""); const legalEntitiesQuery = useQuery({ queryKey: ["master-data", "legal-entities", includeArchived, legalEntityEnabledFilter], enabled: canView, queryFn: () => fetchLegalEntities({ includeArchived, enabled: toEnabledFilter(legalEntityEnabledFilter), }), }); const costCentersQuery = useQuery({ queryKey: [ "master-data", "cost-centers", includeArchived, costCenterEnabledFilter, costCenterLegalEntityFilter, ], enabled: canView, queryFn: () => fetchCostCenters({ includeArchived, enabled: toEnabledFilter(costCenterEnabledFilter), legalEntityId: costCenterLegalEntityFilter || undefined, }), }); const documentCategoriesQuery = useQuery({ queryKey: [ "master-data", "document-categories", includeArchived, documentCategoryEnabledFilter, documentCategoryEntityTypeFilter, ], enabled: canView, queryFn: () => fetchDocumentCategories({ includeArchived, enabled: toEnabledFilter(documentCategoryEnabledFilter), entityType: documentCategoryEntityTypeFilter || undefined, }), }); const legalEntities = legalEntitiesQuery.data?.items ?? []; const costCenters = costCentersQuery.data?.items ?? []; const documentCategories = documentCategoriesQuery.data?.items ?? []; const availableParentCostCenters = useMemo(() => { return costCenters.filter( (item) => !item.archivedAt && item.legalEntityId === costCenterLegalEntityId, ); }, [costCenterLegalEntityId, costCenters]); const invalidateMasterData = async () => { await Promise.all([ queryClient.invalidateQueries({ queryKey: ["master-data", "legal-entities"] }), queryClient.invalidateQueries({ queryKey: ["master-data", "cost-centers"] }), queryClient.invalidateQueries({ queryKey: ["master-data", "document-categories"] }), ]); }; const legalEntityCreateMutation = useMutation({ mutationFn: async () => { return createLegalEntity({ code: legalEntityCode, name: legalEntityName, shortName: legalEntityShortName || null, currencyCode: legalEntityCurrency, taxNo: legalEntityTaxNo || null, }); }, onSuccess: async () => { await invalidateMasterData(); setLegalEntityCode(""); setLegalEntityName(""); setLegalEntityShortName(""); setLegalEntityCurrency("CNY"); setLegalEntityTaxNo(""); setError(""); toast.success("法体已创建"); }, onError: (mutationError) => { const message = resolveError(mutationError); setError(message); toast.error(message); }, }); const costCenterCreateMutation = useMutation({ mutationFn: async () => { return createCostCenter({ legalEntityId: costCenterLegalEntityId, code: costCenterCode, name: costCenterName, parentId: costCenterParentId || null, }); }, onSuccess: async () => { await invalidateMasterData(); setCostCenterLegalEntityId(""); setCostCenterCode(""); setCostCenterName(""); setCostCenterParentId(""); setError(""); toast.success("成本中心已创建"); }, onError: (mutationError) => { const message = resolveError(mutationError); setError(message); toast.error(message); }, }); const documentCategoryCreateMutation = useMutation({ mutationFn: async () => { return createDocumentCategory({ code: documentCategoryCode, name: documentCategoryName, entityType: documentCategoryEntityType, sequencePrefix: documentCategorySequencePrefix, numberLength: Number(documentCategoryNumberLength || 4), resetPeriod: documentCategoryResetPeriod, defaultWorkflowKey: documentCategoryWorkflowKey || null, }); }, onSuccess: async () => { await invalidateMasterData(); setDocumentCategoryCode(""); setDocumentCategoryName(""); setDocumentCategoryEntityType("reimbursement"); setDocumentCategorySequencePrefix("BX"); setDocumentCategoryNumberLength("4"); setDocumentCategoryResetPeriod("yearly"); setDocumentCategoryWorkflowKey(""); setError(""); toast.success("单据分类已创建"); }, onError: (mutationError) => { const message = resolveError(mutationError); setError(message); toast.error(message); }, }); const archiveMutation = useMutation({ mutationFn: async (target: NonNullable) => { if (target.kind === "legal-entity") { return archiveLegalEntity(target.id); } if (target.kind === "cost-center") { return archiveCostCenter(target.id); } return archiveDocumentCategory(target.id); }, onSuccess: async () => { await invalidateMasterData(); setError(""); toast.success("已归档"); }, onError: (mutationError) => { const message = resolveError(mutationError); setError(message); toast.error(message); setConfirmArchive(null); }, }); if (!canView) { return (
所需权限:masterdata.view 或 masterdata.manage
); } return ( setIncludeArchived((value) => !value)}> {includeArchived ? "隐藏归档" : "显示归档"} } /> {error ? (

{error}

) : null} setTab(value as typeof tab)}> 法体 成本中心 单据分类 日历 供应商

法体筛选

{canManage ? (

新建法体

setLegalEntityCode(event.target.value)} placeholder="HYB_CN" />
setLegalEntityName(event.target.value)} placeholder="海音达科技有限公司" />
setLegalEntityShortName(event.target.value)} placeholder="海音达" />
setLegalEntityCurrency(event.target.value)} placeholder="CNY" />
setLegalEntityTaxNo(event.target.value)} placeholder="可选" />
) : null}

法体列表

{legalEntitiesQuery.isLoading ? : null} {!legalEntitiesQuery.isLoading ? ( 编码 名称 简称 币种 状态 操作 {legalEntities.length ? ( legalEntities.map((item) => ( {item.code} {item.name} {item.shortName ?? "-"} {item.currencyCode} {renderEnabledBadge(item)} {canManage && !item.archivedAt ? ( ) : ( - )} )) ) : ( 暂无法体数据 )}
) : null}
{canManage ? (

新建成本中心

setCostCenterCode(event.target.value)} placeholder="RD" />
setCostCenterName(event.target.value)} placeholder="研发中心" />
) : null}

成本中心列表

{costCentersQuery.isLoading ? : null} {!costCentersQuery.isLoading ? ( 编码 名称 所属法体 上级 负责人 状态 操作 {costCenters.length ? ( costCenters.map((item) => ( {item.code} {item.name} {item.legalEntity ? `${item.legalEntity.code} / ${item.legalEntity.name}` : "-"} {item.parent ? `${item.parent.name}` : "-"} {item.owner ? `${item.owner.name} (${item.owner.employeeNo})` : "-"} {renderEnabledBadge(item)} {canManage && !item.archivedAt ? ( ) : ( - )} )) ) : ( 暂无成本中心数据 )}
) : null}
{canManage ? (

新建单据分类

setDocumentCategoryCode(event.target.value)} placeholder="purchase_general" />
setDocumentCategoryName(event.target.value)} placeholder="日常采购申请" />
setDocumentCategorySequencePrefix(event.target.value)} placeholder="CG" />
setDocumentCategoryNumberLength(event.target.value)} />
setDocumentCategoryWorkflowKey(event.target.value)} placeholder="预留字段,不在本轮启用" />
) : null}

单据分类列表

{documentCategoriesQuery.isLoading ? : null} {!documentCategoriesQuery.isLoading ? ( 编码 名称 实体类型 前缀 重置周期 状态 操作 {documentCategories.length ? ( documentCategories.map((item) => ( {item.code} {item.name} {DOCUMENT_ENTITY_TYPE_OPTIONS.find((option) => option.value === item.entityType)?.label ?? item.entityType} {item.sequencePrefix} {RESET_PERIOD_OPTIONS.find((option) => option.value === item.resetPeriod)?.label ?? item.resetPeriod} {renderEnabledBadge(item)} {canManage && !item.archivedAt ? ( ) : ( - )} )) ) : ( 暂无单据分类数据 )}
) : null}
{ if (!open) { setConfirmArchive(null); } }} title="确认归档" description={confirmArchive ? `归档后将无法继续在新业务中使用:${confirmArchive.label}` : undefined} confirmLabel="确认归档" variant="destructive" onConfirm={async () => { if (confirmArchive) { await archiveMutation.mutateAsync(confirmArchive); } }} />
); }