800 行
34 KiB
TypeScript
800 行
34 KiB
TypeScript
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 <StatusBadge status="archived" />;
|
|
}
|
|
if (item.enabled) {
|
|
return <StatusBadge status="active" />;
|
|
}
|
|
return <StatusBadge status="archived" label="已停用" className="badge-neutral" />;
|
|
}
|
|
|
|
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<DocumentCategoryEntityType>("reimbursement");
|
|
const [documentCategorySequencePrefix, setDocumentCategorySequencePrefix] = useState("BX");
|
|
const [documentCategoryNumberLength, setDocumentCategoryNumberLength] = useState("4");
|
|
const [documentCategoryResetPeriod, setDocumentCategoryResetPeriod] =
|
|
useState<DocumentResetPeriod>("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<typeof confirmArchive>) => {
|
|
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 (
|
|
<ListWorkspace>
|
|
<WorkspaceHeader title="主数据" subtitle="你当前没有主数据权限,请联系管理员开通。" />
|
|
<div className="card-std p-4 text-sm text-zinc-600 md:p-5">所需权限:masterdata.view 或 masterdata.manage</div>
|
|
</ListWorkspace>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<ListWorkspace>
|
|
<WorkspaceHeader
|
|
title="主数据"
|
|
subtitle="集中维护法体、成本中心、单据分类、日历与供应商,为预算、采购、请假与资产扩展提供统一骨架。"
|
|
actions={
|
|
<Button variant="secondary" type="button" onClick={() => setIncludeArchived((value) => !value)}>
|
|
{includeArchived ? "隐藏归档" : "显示归档"}
|
|
</Button>
|
|
}
|
|
/>
|
|
|
|
{error ? (
|
|
<p className="rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-600">{error}</p>
|
|
) : null}
|
|
|
|
<Tabs value={tab} onValueChange={(value) => setTab(value as typeof tab)}>
|
|
<TabsList className="flex w-full flex-wrap justify-start gap-1">
|
|
<TabsTrigger value="legal-entities">法体</TabsTrigger>
|
|
<TabsTrigger value="cost-centers">成本中心</TabsTrigger>
|
|
<TabsTrigger value="document-categories">单据分类</TabsTrigger>
|
|
<TabsTrigger value="holiday-calendars">日历</TabsTrigger>
|
|
<TabsTrigger value="suppliers">供应商</TabsTrigger>
|
|
</TabsList>
|
|
|
|
<TabsContent value="legal-entities" className="space-y-6">
|
|
<FilterBar className="space-y-4">
|
|
<div className="flex flex-wrap items-center justify-between gap-3">
|
|
<h3 className="text-base font-semibold text-zinc-900">法体筛选</h3>
|
|
<select
|
|
className="select-std max-w-xs"
|
|
value={legalEntityEnabledFilter}
|
|
onChange={(event) => setLegalEntityEnabledFilter(event.target.value as "" | "true" | "false")}
|
|
>
|
|
<option value="">全部状态</option>
|
|
<option value="true">仅启用</option>
|
|
<option value="false">仅停用</option>
|
|
</select>
|
|
</div>
|
|
</FilterBar>
|
|
|
|
{canManage ? (
|
|
<div className="card-std space-y-4 p-4 md:p-5">
|
|
<h3 className="text-base font-semibold text-zinc-900">新建法体</h3>
|
|
<div className="grid gap-3 md:grid-cols-3">
|
|
<div className="space-y-1">
|
|
<Label htmlFor="legalEntityCode">编码</Label>
|
|
<Input id="legalEntityCode" value={legalEntityCode} onChange={(event) => setLegalEntityCode(event.target.value)} placeholder="HYB_CN" />
|
|
</div>
|
|
<div className="space-y-1">
|
|
<Label htmlFor="legalEntityName">名称</Label>
|
|
<Input id="legalEntityName" value={legalEntityName} onChange={(event) => setLegalEntityName(event.target.value)} placeholder="海音达科技有限公司" />
|
|
</div>
|
|
<div className="space-y-1">
|
|
<Label htmlFor="legalEntityShortName">简称</Label>
|
|
<Input id="legalEntityShortName" value={legalEntityShortName} onChange={(event) => setLegalEntityShortName(event.target.value)} placeholder="海音达" />
|
|
</div>
|
|
<div className="space-y-1">
|
|
<Label htmlFor="legalEntityCurrency">币种</Label>
|
|
<Input id="legalEntityCurrency" value={legalEntityCurrency} onChange={(event) => setLegalEntityCurrency(event.target.value)} placeholder="CNY" />
|
|
</div>
|
|
<div className="space-y-1">
|
|
<Label htmlFor="legalEntityTaxNo">税号</Label>
|
|
<Input id="legalEntityTaxNo" value={legalEntityTaxNo} onChange={(event) => setLegalEntityTaxNo(event.target.value)} placeholder="可选" />
|
|
</div>
|
|
<div className="flex items-end">
|
|
<Button
|
|
type="button"
|
|
disabled={legalEntityCreateMutation.isPending || !legalEntityCode.trim() || !legalEntityName.trim() || !legalEntityCurrency.trim()}
|
|
onClick={() => legalEntityCreateMutation.mutate()}
|
|
>
|
|
{legalEntityCreateMutation.isPending ? "创建中..." : "创建法体"}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
) : null}
|
|
|
|
<div className="card-std space-y-3 p-4 md:p-5">
|
|
<h3 className="text-base font-semibold text-zinc-900">法体列表</h3>
|
|
{legalEntitiesQuery.isLoading ? <TableSkeleton columns={6} rows={4} /> : null}
|
|
{!legalEntitiesQuery.isLoading ? (
|
|
<PrimaryTable>
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow>
|
|
<TableHead>编码</TableHead>
|
|
<TableHead>名称</TableHead>
|
|
<TableHead>简称</TableHead>
|
|
<TableHead>币种</TableHead>
|
|
<TableHead>状态</TableHead>
|
|
<TableHead>操作</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{legalEntities.length ? (
|
|
legalEntities.map((item) => (
|
|
<TableRow key={item.id}>
|
|
<TableCell className="font-medium">{item.code}</TableCell>
|
|
<TableCell>{item.name}</TableCell>
|
|
<TableCell>{item.shortName ?? "-"}</TableCell>
|
|
<TableCell>{item.currencyCode}</TableCell>
|
|
<TableCell>{renderEnabledBadge(item)}</TableCell>
|
|
<TableCell>
|
|
{canManage && !item.archivedAt ? (
|
|
<Button
|
|
type="button"
|
|
variant="secondary"
|
|
onClick={() =>
|
|
setConfirmArchive({
|
|
kind: "legal-entity",
|
|
id: item.id,
|
|
label: `${item.code} / ${item.name}`,
|
|
})
|
|
}
|
|
>
|
|
归档
|
|
</Button>
|
|
) : (
|
|
<span className="text-sm text-zinc-400">-</span>
|
|
)}
|
|
</TableCell>
|
|
</TableRow>
|
|
))
|
|
) : (
|
|
<TableRow>
|
|
<TableCell colSpan={6} className="text-center text-sm text-zinc-500">
|
|
暂无法体数据
|
|
</TableCell>
|
|
</TableRow>
|
|
)}
|
|
</TableBody>
|
|
</Table>
|
|
</PrimaryTable>
|
|
) : null}
|
|
</div>
|
|
</TabsContent>
|
|
|
|
<TabsContent value="cost-centers" className="space-y-6">
|
|
<FilterBar className="space-y-4">
|
|
<div className="grid gap-3 md:grid-cols-3">
|
|
<select
|
|
className="select-std"
|
|
value={costCenterEnabledFilter}
|
|
onChange={(event) => setCostCenterEnabledFilter(event.target.value as "" | "true" | "false")}
|
|
>
|
|
<option value="">全部状态</option>
|
|
<option value="true">仅启用</option>
|
|
<option value="false">仅停用</option>
|
|
</select>
|
|
<select
|
|
className="select-std"
|
|
value={costCenterLegalEntityFilter}
|
|
onChange={(event) => setCostCenterLegalEntityFilter(event.target.value)}
|
|
>
|
|
<option value="">全部法体</option>
|
|
{legalEntities
|
|
.filter((item) => !item.archivedAt)
|
|
.map((item) => (
|
|
<option key={item.id} value={item.id}>
|
|
{item.code} / {item.name}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
</FilterBar>
|
|
|
|
{canManage ? (
|
|
<div className="card-std space-y-4 p-4 md:p-5">
|
|
<h3 className="text-base font-semibold text-zinc-900">新建成本中心</h3>
|
|
<div className="grid gap-3 md:grid-cols-4">
|
|
<div className="space-y-1 md:col-span-2">
|
|
<Label htmlFor="costCenterLegalEntity">所属法体</Label>
|
|
<select
|
|
id="costCenterLegalEntity"
|
|
className="select-std"
|
|
value={costCenterLegalEntityId}
|
|
onChange={(event) => {
|
|
setCostCenterLegalEntityId(event.target.value);
|
|
setCostCenterParentId("");
|
|
}}
|
|
>
|
|
<option value="">请选择法体</option>
|
|
{legalEntities
|
|
.filter((item) => !item.archivedAt)
|
|
.map((item) => (
|
|
<option key={item.id} value={item.id}>
|
|
{item.code} / {item.name}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
<div className="space-y-1">
|
|
<Label htmlFor="costCenterCode">编码</Label>
|
|
<Input id="costCenterCode" value={costCenterCode} onChange={(event) => setCostCenterCode(event.target.value)} placeholder="RD" />
|
|
</div>
|
|
<div className="space-y-1">
|
|
<Label htmlFor="costCenterName">名称</Label>
|
|
<Input id="costCenterName" value={costCenterName} onChange={(event) => setCostCenterName(event.target.value)} placeholder="研发中心" />
|
|
</div>
|
|
<div className="space-y-1 md:col-span-3">
|
|
<Label htmlFor="costCenterParent">上级成本中心(可选)</Label>
|
|
<select
|
|
id="costCenterParent"
|
|
className="select-std"
|
|
value={costCenterParentId}
|
|
onChange={(event) => setCostCenterParentId(event.target.value)}
|
|
disabled={!costCenterLegalEntityId}
|
|
>
|
|
<option value="">无</option>
|
|
{availableParentCostCenters.map((item: CostCenterItem) => (
|
|
<option key={item.id} value={item.id}>
|
|
{item.code} / {item.name}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
<div className="flex items-end">
|
|
<Button
|
|
type="button"
|
|
disabled={
|
|
costCenterCreateMutation.isPending ||
|
|
!costCenterLegalEntityId ||
|
|
!costCenterCode.trim() ||
|
|
!costCenterName.trim()
|
|
}
|
|
onClick={() => costCenterCreateMutation.mutate()}
|
|
>
|
|
{costCenterCreateMutation.isPending ? "创建中..." : "创建成本中心"}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
) : null}
|
|
|
|
<div className="card-std space-y-3 p-4 md:p-5">
|
|
<h3 className="text-base font-semibold text-zinc-900">成本中心列表</h3>
|
|
{costCentersQuery.isLoading ? <TableSkeleton columns={7} rows={4} /> : null}
|
|
{!costCentersQuery.isLoading ? (
|
|
<PrimaryTable>
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow>
|
|
<TableHead>编码</TableHead>
|
|
<TableHead>名称</TableHead>
|
|
<TableHead>所属法体</TableHead>
|
|
<TableHead>上级</TableHead>
|
|
<TableHead>负责人</TableHead>
|
|
<TableHead>状态</TableHead>
|
|
<TableHead>操作</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{costCenters.length ? (
|
|
costCenters.map((item) => (
|
|
<TableRow key={item.id}>
|
|
<TableCell className="font-medium">{item.code}</TableCell>
|
|
<TableCell>{item.name}</TableCell>
|
|
<TableCell>{item.legalEntity ? `${item.legalEntity.code} / ${item.legalEntity.name}` : "-"}</TableCell>
|
|
<TableCell>{item.parent ? `${item.parent.name}` : "-"}</TableCell>
|
|
<TableCell>{item.owner ? `${item.owner.name} (${item.owner.employeeNo})` : "-"}</TableCell>
|
|
<TableCell>{renderEnabledBadge(item)}</TableCell>
|
|
<TableCell>
|
|
{canManage && !item.archivedAt ? (
|
|
<Button
|
|
type="button"
|
|
variant="secondary"
|
|
onClick={() =>
|
|
setConfirmArchive({
|
|
kind: "cost-center",
|
|
id: item.id,
|
|
label: `${item.code} / ${item.name}`,
|
|
})
|
|
}
|
|
>
|
|
归档
|
|
</Button>
|
|
) : (
|
|
<span className="text-sm text-zinc-400">-</span>
|
|
)}
|
|
</TableCell>
|
|
</TableRow>
|
|
))
|
|
) : (
|
|
<TableRow>
|
|
<TableCell colSpan={7} className="text-center text-sm text-zinc-500">
|
|
暂无成本中心数据
|
|
</TableCell>
|
|
</TableRow>
|
|
)}
|
|
</TableBody>
|
|
</Table>
|
|
</PrimaryTable>
|
|
) : null}
|
|
</div>
|
|
</TabsContent>
|
|
|
|
<TabsContent value="document-categories" className="space-y-6">
|
|
<FilterBar className="space-y-4">
|
|
<div className="grid gap-3 md:grid-cols-3">
|
|
<select
|
|
className="select-std"
|
|
value={documentCategoryEnabledFilter}
|
|
onChange={(event) =>
|
|
setDocumentCategoryEnabledFilter(event.target.value as "" | "true" | "false")
|
|
}
|
|
>
|
|
<option value="">全部状态</option>
|
|
<option value="true">仅启用</option>
|
|
<option value="false">仅停用</option>
|
|
</select>
|
|
<select
|
|
className="select-std"
|
|
value={documentCategoryEntityTypeFilter}
|
|
onChange={(event) => setDocumentCategoryEntityTypeFilter(event.target.value as "" | DocumentCategoryEntityType)}
|
|
>
|
|
<option value="">全部实体</option>
|
|
{DOCUMENT_ENTITY_TYPE_OPTIONS.map((item) => (
|
|
<option key={item.value} value={item.value}>
|
|
{item.label}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
</FilterBar>
|
|
|
|
{canManage ? (
|
|
<div className="card-std space-y-4 p-4 md:p-5">
|
|
<h3 className="text-base font-semibold text-zinc-900">新建单据分类</h3>
|
|
<div className="grid gap-3 md:grid-cols-4">
|
|
<div className="space-y-1">
|
|
<Label htmlFor="documentCategoryCode">编码</Label>
|
|
<Input id="documentCategoryCode" value={documentCategoryCode} onChange={(event) => setDocumentCategoryCode(event.target.value)} placeholder="purchase_general" />
|
|
</div>
|
|
<div className="space-y-1">
|
|
<Label htmlFor="documentCategoryName">名称</Label>
|
|
<Input id="documentCategoryName" value={documentCategoryName} onChange={(event) => setDocumentCategoryName(event.target.value)} placeholder="日常采购申请" />
|
|
</div>
|
|
<div className="space-y-1">
|
|
<Label htmlFor="documentCategoryEntityType">实体类型</Label>
|
|
<select
|
|
id="documentCategoryEntityType"
|
|
className="select-std"
|
|
value={documentCategoryEntityType}
|
|
onChange={(event) => setDocumentCategoryEntityType(event.target.value as DocumentCategoryEntityType)}
|
|
>
|
|
{DOCUMENT_ENTITY_TYPE_OPTIONS.map((item) => (
|
|
<option key={item.value} value={item.value}>
|
|
{item.label}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
<div className="space-y-1">
|
|
<Label htmlFor="documentCategoryPrefix">流水号前缀</Label>
|
|
<Input id="documentCategoryPrefix" value={documentCategorySequencePrefix} onChange={(event) => setDocumentCategorySequencePrefix(event.target.value)} placeholder="CG" />
|
|
</div>
|
|
<div className="space-y-1">
|
|
<Label htmlFor="documentCategoryNumberLength">流水号位数</Label>
|
|
<Input id="documentCategoryNumberLength" type="number" min={2} max={8} value={documentCategoryNumberLength} onChange={(event) => setDocumentCategoryNumberLength(event.target.value)} />
|
|
</div>
|
|
<div className="space-y-1">
|
|
<Label htmlFor="documentCategoryResetPeriod">重置周期</Label>
|
|
<select
|
|
id="documentCategoryResetPeriod"
|
|
className="select-std"
|
|
value={documentCategoryResetPeriod}
|
|
onChange={(event) => setDocumentCategoryResetPeriod(event.target.value as DocumentResetPeriod)}
|
|
>
|
|
{RESET_PERIOD_OPTIONS.map((item) => (
|
|
<option key={item.value} value={item.value}>
|
|
{item.label}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
<div className="space-y-1 md:col-span-2">
|
|
<Label htmlFor="documentCategoryWorkflowKey">默认工作流键(可选)</Label>
|
|
<Input id="documentCategoryWorkflowKey" value={documentCategoryWorkflowKey} onChange={(event) => setDocumentCategoryWorkflowKey(event.target.value)} placeholder="预留字段,不在本轮启用" />
|
|
</div>
|
|
<div className="flex items-end">
|
|
<Button
|
|
type="button"
|
|
disabled={
|
|
documentCategoryCreateMutation.isPending ||
|
|
!documentCategoryCode.trim() ||
|
|
!documentCategoryName.trim() ||
|
|
!documentCategorySequencePrefix.trim()
|
|
}
|
|
onClick={() => documentCategoryCreateMutation.mutate()}
|
|
>
|
|
{documentCategoryCreateMutation.isPending ? "创建中..." : "创建单据分类"}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
) : null}
|
|
|
|
<div className="card-std space-y-3 p-4 md:p-5">
|
|
<h3 className="text-base font-semibold text-zinc-900">单据分类列表</h3>
|
|
{documentCategoriesQuery.isLoading ? <TableSkeleton columns={7} rows={4} /> : null}
|
|
{!documentCategoriesQuery.isLoading ? (
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow>
|
|
<TableHead>编码</TableHead>
|
|
<TableHead>名称</TableHead>
|
|
<TableHead>实体类型</TableHead>
|
|
<TableHead>前缀</TableHead>
|
|
<TableHead>重置周期</TableHead>
|
|
<TableHead>状态</TableHead>
|
|
<TableHead>操作</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{documentCategories.length ? (
|
|
documentCategories.map((item) => (
|
|
<TableRow key={item.id}>
|
|
<TableCell className="font-medium">{item.code}</TableCell>
|
|
<TableCell>{item.name}</TableCell>
|
|
<TableCell>
|
|
{DOCUMENT_ENTITY_TYPE_OPTIONS.find((option) => option.value === item.entityType)?.label ?? item.entityType}
|
|
</TableCell>
|
|
<TableCell>{item.sequencePrefix}</TableCell>
|
|
<TableCell>
|
|
{RESET_PERIOD_OPTIONS.find((option) => option.value === item.resetPeriod)?.label ?? item.resetPeriod}
|
|
</TableCell>
|
|
<TableCell>{renderEnabledBadge(item)}</TableCell>
|
|
<TableCell>
|
|
{canManage && !item.archivedAt ? (
|
|
<Button
|
|
type="button"
|
|
variant="secondary"
|
|
onClick={() =>
|
|
setConfirmArchive({
|
|
kind: "document-category",
|
|
id: item.id,
|
|
label: `${item.code} / ${item.name}`,
|
|
})
|
|
}
|
|
>
|
|
归档
|
|
</Button>
|
|
) : (
|
|
<span className="text-sm text-zinc-400">-</span>
|
|
)}
|
|
</TableCell>
|
|
</TableRow>
|
|
))
|
|
) : (
|
|
<TableRow>
|
|
<TableCell colSpan={7} className="text-center text-sm text-zinc-500">
|
|
暂无单据分类数据
|
|
</TableCell>
|
|
</TableRow>
|
|
)}
|
|
</TableBody>
|
|
</Table>
|
|
) : null}
|
|
</div>
|
|
</TabsContent>
|
|
|
|
<TabsContent value="holiday-calendars" className="space-y-6">
|
|
<HolidayCalendarTab
|
|
canManage={canManage}
|
|
includeArchived={includeArchived}
|
|
legalEntities={legalEntities}
|
|
/>
|
|
</TabsContent>
|
|
<TabsContent value="suppliers" className="space-y-6">
|
|
<SupplierTab
|
|
canManage={canManage}
|
|
includeArchived={includeArchived}
|
|
legalEntities={legalEntities}
|
|
/>
|
|
</TabsContent>
|
|
</Tabs>
|
|
|
|
<ConfirmDialog
|
|
open={Boolean(confirmArchive)}
|
|
onOpenChange={(open) => {
|
|
if (!open) {
|
|
setConfirmArchive(null);
|
|
}
|
|
}}
|
|
title="确认归档"
|
|
description={confirmArchive ? `归档后将无法继续在新业务中使用:${confirmArchive.label}` : undefined}
|
|
confirmLabel="确认归档"
|
|
variant="destructive"
|
|
onConfirm={async () => {
|
|
if (confirmArchive) {
|
|
await archiveMutation.mutateAsync(confirmArchive);
|
|
}
|
|
}}
|
|
/>
|
|
</ListWorkspace>
|
|
);
|
|
}
|