feat(management-navigation-single-company-ux): 收敛管理导航与单公司配置体验
这个提交包含在:
@@ -495,7 +495,7 @@ export function AssetRegistryPage() {
|
||||
throw new Error("请选择资产分类");
|
||||
}
|
||||
if (!assetForm.legalEntityId) {
|
||||
throw new Error("请选择法体");
|
||||
throw new Error("请选择公司");
|
||||
}
|
||||
if (!assetForm.costCenterId) {
|
||||
throw new Error("请选择成本中心");
|
||||
@@ -935,7 +935,7 @@ export function AssetRegistryPage() {
|
||||
setLegalEntityFilter(event.target.value);
|
||||
setCostCenterFilter("");
|
||||
}}>
|
||||
<option value="">全部法体</option>
|
||||
<option value="">全部公司</option>
|
||||
{(legalEntitiesQuery.data?.items ?? []).map((item) => (
|
||||
<option key={item.id} value={item.id}>
|
||||
{formatTextWithCompactLongNumbers(item.name)}
|
||||
@@ -1217,7 +1217,7 @@ export function AssetRegistryPage() {
|
||||
</select>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-zinc-700">法体</label>
|
||||
<label className="text-sm font-medium text-zinc-700">公司</label>
|
||||
<select data-testid="asset-legal-entity" className="select-std" value={assetForm.legalEntityId} disabled={!canEditCurrent} onChange={(event) => {
|
||||
const legalEntityId = event.target.value;
|
||||
setAssetForm((current) => ({
|
||||
@@ -1228,7 +1228,7 @@ export function AssetRegistryPage() {
|
||||
: "",
|
||||
}));
|
||||
}}>
|
||||
<option value="">请选择法体</option>
|
||||
<option value="">请选择公司</option>
|
||||
{(legalEntitiesQuery.data?.items ?? []).map((item) => (
|
||||
<option key={item.id} value={item.id}>
|
||||
{item.name}
|
||||
|
||||
@@ -74,9 +74,9 @@ function describeScope(item: BudgetPool) {
|
||||
}
|
||||
|
||||
if (item.scopeType === "cost_center_scoped") {
|
||||
const legalEntityName = item.legalEntity?.name ?? "未知法体";
|
||||
const legalEntityName = item.legalEntity?.name ?? "未知公司";
|
||||
const costCenterName = item.costCenter?.name ?? "未知成本中心";
|
||||
return `法体:${legalEntityName} / 成本中心:${costCenterName}`;
|
||||
return `公司:${legalEntityName} / 成本中心:${costCenterName}`;
|
||||
}
|
||||
|
||||
if (item.scopeType === "global") {
|
||||
@@ -339,7 +339,7 @@ export function BudgetsPage() {
|
||||
function describeRuleScope(rule: BudgetControlRule) {
|
||||
const parts: string[] = [];
|
||||
if (rule.legalEntityId) {
|
||||
parts.push(`法体:${legalEntityMap.get(rule.legalEntityId) ?? rule.legalEntityId}`);
|
||||
parts.push(`公司:${legalEntityMap.get(rule.legalEntityId) ?? rule.legalEntityId}`);
|
||||
}
|
||||
if (rule.departmentId) {
|
||||
parts.push(`部门:${departmentMap.get(rule.departmentId) ?? rule.departmentId}`);
|
||||
@@ -441,7 +441,7 @@ export function BudgetsPage() {
|
||||
setPage(1);
|
||||
}}
|
||||
>
|
||||
<option value="">全部法体</option>
|
||||
<option value="">全部公司</option>
|
||||
{(legalEntitiesQuery.data?.items ?? []).map((legalEntity) => (
|
||||
<option key={legalEntity.id} value={legalEntity.id}>
|
||||
{legalEntity.name}
|
||||
@@ -519,7 +519,7 @@ export function BudgetsPage() {
|
||||
>
|
||||
<option value="global">全局预算</option>
|
||||
<option value="department_legacy">部门预算</option>
|
||||
<option value="cost_center_scoped">法体 + 成本中心预算</option>
|
||||
<option value="cost_center_scoped">公司 + 成本中心预算</option>
|
||||
</select>
|
||||
|
||||
{formScopeType === "department_legacy" ? (
|
||||
@@ -547,7 +547,7 @@ export function BudgetsPage() {
|
||||
setFormCostCenterId("");
|
||||
}}
|
||||
>
|
||||
<option value="">选择法体</option>
|
||||
<option value="">选择公司</option>
|
||||
{(legalEntitiesQuery.data?.items ?? []).map((legalEntity) => (
|
||||
<option key={legalEntity.id} value={legalEntity.id}>
|
||||
{legalEntity.name}
|
||||
@@ -763,7 +763,7 @@ export function BudgetsPage() {
|
||||
value={ruleForm.legalEntityId}
|
||||
onChange={(event) => setRuleForm((current) => ({ ...current, legalEntityId: event.target.value }))}
|
||||
>
|
||||
<option value="">不限法体</option>
|
||||
<option value="">不限公司</option>
|
||||
{(legalEntitiesQuery.data?.items ?? []).map((item) => (
|
||||
<option key={item.id} value={item.id}>
|
||||
{item.name}
|
||||
|
||||
@@ -566,7 +566,7 @@ export function CutoverConsolePage() {
|
||||
scopeKey: event.target.value,
|
||||
}))
|
||||
}
|
||||
placeholder="部门 ID / 法体 ID / 业务域"
|
||||
placeholder="部门 ID / 公司 ID / 业务域"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ function scopeLabel(scopeType: InventoryCheckScopeType) {
|
||||
case "all":
|
||||
return "全量盘点";
|
||||
case "legal_entity":
|
||||
return "法体盘点";
|
||||
return "公司盘点";
|
||||
case "cost_center":
|
||||
return "成本中心盘点";
|
||||
case "location":
|
||||
@@ -278,7 +278,7 @@ export function InventoryChecksPage() {
|
||||
setScopeRefId("");
|
||||
}}>
|
||||
<option value="all">全量盘点</option>
|
||||
<option value="legal_entity">法体盘点</option>
|
||||
<option value="legal_entity">公司盘点</option>
|
||||
<option value="cost_center">成本中心盘点</option>
|
||||
<option value="location">位置盘点</option>
|
||||
</select>
|
||||
|
||||
@@ -30,6 +30,7 @@ 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";
|
||||
import { resolveSingleCompanyState } from "../features/company/singleCompany";
|
||||
|
||||
const DOCUMENT_ENTITY_TYPE_OPTIONS: Array<{ value: DocumentCategoryEntityType; label: string }> = [
|
||||
{ value: "reimbursement", label: "报销" },
|
||||
@@ -132,6 +133,12 @@ export function MasterDataPage() {
|
||||
}),
|
||||
});
|
||||
|
||||
const activeCompaniesQuery = useQuery({
|
||||
queryKey: ["master-data", "legal-entities", "active-company-state"],
|
||||
enabled: canView,
|
||||
queryFn: () => fetchLegalEntities({ enabled: true }),
|
||||
});
|
||||
|
||||
const costCentersQuery = useQuery({
|
||||
queryKey: [
|
||||
"master-data",
|
||||
@@ -167,6 +174,8 @@ export function MasterDataPage() {
|
||||
});
|
||||
|
||||
const legalEntities = legalEntitiesQuery.data?.items ?? [];
|
||||
const activeCompanies = activeCompaniesQuery.data?.items ?? [];
|
||||
const companyState = resolveSingleCompanyState(activeCompanies);
|
||||
const costCenters = costCentersQuery.data?.items ?? [];
|
||||
const documentCategories = documentCategoriesQuery.data?.items ?? [];
|
||||
const masterDataStats = {
|
||||
@@ -211,7 +220,7 @@ export function MasterDataPage() {
|
||||
setLegalEntityCurrency("CNY");
|
||||
setLegalEntityTaxNo("");
|
||||
setError("");
|
||||
toast.success("法体已创建");
|
||||
toast.success("公司信息已创建");
|
||||
},
|
||||
onError: (mutationError) => {
|
||||
const message = resolveError(mutationError);
|
||||
@@ -312,7 +321,7 @@ export function MasterDataPage() {
|
||||
<ListWorkspace>
|
||||
<WorkspaceHeader
|
||||
title="主数据"
|
||||
subtitle="集中维护法体、成本中心、单据分类、日历与供应商,为预算、采购、请假与资产扩展提供统一骨架。"
|
||||
subtitle="集中维护公司信息、成本中心、单据分类、日历与供应商,为预算、采购、请假与资产扩展提供统一基础资料。"
|
||||
actions={
|
||||
<Button variant="secondary" type="button" onClick={() => setIncludeArchived((value) => !value)}>
|
||||
{includeArchived ? "隐藏归档" : "显示归档"}
|
||||
@@ -325,7 +334,7 @@ export function MasterDataPage() {
|
||||
) : null}
|
||||
|
||||
<div className="finance-status-strip" data-testid="master-data-summary">
|
||||
<div className="finance-metric-card"><div className="finance-metric-card__label">法体</div><div className="finance-metric-card__value">{masterDataStats.legalEntities}</div></div>
|
||||
<div className="finance-metric-card"><div className="finance-metric-card__label">公司信息</div><div className="finance-metric-card__value">{masterDataStats.legalEntities}</div></div>
|
||||
<div className="finance-metric-card"><div className="finance-metric-card__label">成本中心</div><div className="finance-metric-card__value">{masterDataStats.costCenters}</div></div>
|
||||
<div className="finance-metric-card"><div className="finance-metric-card__label">单据分类</div><div className="finance-metric-card__value">{masterDataStats.documentCategories}</div></div>
|
||||
<div className="finance-metric-card"><div className="finance-metric-card__label">归档项</div><div className="finance-metric-card__value">{masterDataStats.archived}</div></div>
|
||||
@@ -334,7 +343,7 @@ export function MasterDataPage() {
|
||||
|
||||
<Tabs value={tab} onValueChange={(value) => setTab(value as typeof tab)}>
|
||||
<TabsList className="ops-command-bar__tabs !w-full !justify-start">
|
||||
<TabsTrigger value="legal-entities">法体</TabsTrigger>
|
||||
<TabsTrigger value="legal-entities">公司信息</TabsTrigger>
|
||||
<TabsTrigger value="cost-centers">成本中心</TabsTrigger>
|
||||
<TabsTrigger value="document-categories">单据分类</TabsTrigger>
|
||||
<TabsTrigger value="holiday-calendars">日历</TabsTrigger>
|
||||
@@ -344,7 +353,7 @@ export function MasterDataPage() {
|
||||
<TabsContent value="legal-entities" className="space-y-4">
|
||||
<FilterBar className="ops-command-bar !p-3 md:!p-3.5">
|
||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||
<h3 className="text-base font-semibold text-zinc-900">法体筛选</h3>
|
||||
<h3 className="text-base font-semibold text-zinc-900">公司信息筛选</h3>
|
||||
<select
|
||||
className="select-std max-w-xs"
|
||||
value={legalEntityEnabledFilter}
|
||||
@@ -357,9 +366,18 @@ export function MasterDataPage() {
|
||||
</div>
|
||||
</FilterBar>
|
||||
|
||||
{canManage ? (
|
||||
{activeCompaniesQuery.isError ? (
|
||||
<div className="admin-command-panel border-red-200 bg-red-50/70 text-sm text-red-600">
|
||||
公司信息读取失败,请刷新后重试。
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{canManage && !activeCompaniesQuery.isLoading && !activeCompaniesQuery.isError && companyState.kind === "empty" ? (
|
||||
<div className="admin-command-panel space-y-4">
|
||||
<h3 className="text-base font-semibold text-zinc-900">新建法体</h3>
|
||||
<div>
|
||||
<h3 className="text-base font-semibold text-zinc-900">完善公司信息</h3>
|
||||
<p className="mt-1 text-sm text-zinc-600">系统仅服务当前公司。完成后,企业银行账户等配置会自动关联这家公司。</p>
|
||||
</div>
|
||||
<div className="grid gap-3 md:grid-cols-3">
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="legalEntityCode">编码</Label>
|
||||
@@ -387,15 +405,29 @@ export function MasterDataPage() {
|
||||
disabled={legalEntityCreateMutation.isPending || !legalEntityCode.trim() || !legalEntityName.trim() || !legalEntityCurrency.trim()}
|
||||
onClick={() => legalEntityCreateMutation.mutate()}
|
||||
>
|
||||
{legalEntityCreateMutation.isPending ? "创建中..." : "创建法体"}
|
||||
{legalEntityCreateMutation.isPending ? "创建中..." : "保存公司信息"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{!activeCompaniesQuery.isLoading && !activeCompaniesQuery.isError && companyState.kind === "single" ? (
|
||||
<div className="admin-command-panel">
|
||||
<div className="text-sm font-medium text-zinc-900">当前公司:{companyState.company.name}</div>
|
||||
<div className="mt-1 text-sm text-zinc-600">系统按单公司运行,不需要选择或切换公司。</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{!activeCompaniesQuery.isLoading && !activeCompaniesQuery.isError && companyState.kind === "multiple" ? (
|
||||
<div className="admin-command-panel border-red-200 bg-red-50/70">
|
||||
<div className="text-sm font-medium text-red-700">检测到多条启用的公司信息</div>
|
||||
<div className="mt-1 text-sm text-red-600">系统不会自动选择或合并,请先停用多余记录后再继续关联配置。</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="admin-command-panel space-y-4">
|
||||
<h3 className="text-base font-semibold text-zinc-900">法体列表</h3>
|
||||
<h3 className="text-base font-semibold text-zinc-900">公司信息</h3>
|
||||
{legalEntitiesQuery.isLoading ? <TableSkeleton columns={6} rows={4} /> : null}
|
||||
{!legalEntitiesQuery.isLoading ? (
|
||||
<PrimaryTable className="ops-table-focus">
|
||||
@@ -443,7 +475,7 @@ export function MasterDataPage() {
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell colSpan={6} className="text-center text-sm text-zinc-500">
|
||||
暂无法体数据
|
||||
暂无公司信息
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
@@ -471,7 +503,7 @@ export function MasterDataPage() {
|
||||
value={costCenterLegalEntityFilter}
|
||||
onChange={(event) => setCostCenterLegalEntityFilter(event.target.value)}
|
||||
>
|
||||
<option value="">全部法体</option>
|
||||
<option value="">全部公司</option>
|
||||
{legalEntities
|
||||
.filter((item) => !item.archivedAt)
|
||||
.map((item) => (
|
||||
@@ -488,7 +520,7 @@ export function MasterDataPage() {
|
||||
<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>
|
||||
<Label htmlFor="costCenterLegalEntity">所属公司</Label>
|
||||
<select
|
||||
id="costCenterLegalEntity"
|
||||
className="select-std"
|
||||
@@ -498,7 +530,7 @@ export function MasterDataPage() {
|
||||
setCostCenterParentId("");
|
||||
}}
|
||||
>
|
||||
<option value="">请选择法体</option>
|
||||
<option value="">请选择公司</option>
|
||||
{legalEntities
|
||||
.filter((item) => !item.archivedAt)
|
||||
.map((item) => (
|
||||
@@ -561,7 +593,7 @@ export function MasterDataPage() {
|
||||
<TableRow>
|
||||
<TableHead>编码</TableHead>
|
||||
<TableHead>名称</TableHead>
|
||||
<TableHead>所属法体</TableHead>
|
||||
<TableHead>所属公司</TableHead>
|
||||
<TableHead>上级</TableHead>
|
||||
<TableHead>负责人</TableHead>
|
||||
<TableHead>状态</TableHead>
|
||||
|
||||
@@ -102,7 +102,7 @@ function assetTypeLabel(assetType: AssetItem["assetType"]) {
|
||||
|
||||
function inventoryScopeLabel(inventoryCheck: InventoryCheck) {
|
||||
if (inventoryCheck.scopeType === "all") return "全量盘点";
|
||||
if (inventoryCheck.scopeType === "legal_entity") return "法体盘点";
|
||||
if (inventoryCheck.scopeType === "legal_entity") return "公司盘点";
|
||||
if (inventoryCheck.scopeType === "cost_center") return "成本中心盘点";
|
||||
return "位置盘点";
|
||||
}
|
||||
@@ -559,7 +559,7 @@ export function MobileAssetScanPage() {
|
||||
<div className="mobile-info-grid">
|
||||
<div className="mobile-subcard text-sm">类型:{assetTypeLabel(selectedAsset.assetType)}</div>
|
||||
<div className="mobile-subcard text-sm">状态:{statusLabel(selectedAsset.status)}</div>
|
||||
<div className="mobile-subcard text-sm">法体:{selectedAsset.legalEntity?.name ?? "-"}</div>
|
||||
<div className="mobile-subcard text-sm">公司:{selectedAsset.legalEntity?.name ?? "-"}</div>
|
||||
<div className="mobile-subcard text-sm">成本中心:{selectedAsset.costCenter?.name ?? "-"}</div>
|
||||
<div className="mobile-subcard text-sm">位置:{selectedAsset.location ?? "-"}</div>
|
||||
<div className="mobile-subcard text-sm">归属人:{selectedAsset.assignee?.name ?? "未分配"}</div>
|
||||
|
||||
@@ -52,7 +52,7 @@ function resolveMobilePurchaseEvidence(detail: {
|
||||
projectId: string | null;
|
||||
}) {
|
||||
if (!detail.supplierId) return "待补供应商";
|
||||
if (!detail.legalEntityId || !detail.costCenterId) return "待补法体 / 成本中心";
|
||||
if (!detail.legalEntityId || !detail.costCenterId) return "待补公司 / 成本中心";
|
||||
if (!detail.documentCategoryId) return "待补单据分类";
|
||||
if (!detail.budgetPoolId) return "预算池未匹配";
|
||||
if (!detail.projectId) return "未关联项目";
|
||||
@@ -167,7 +167,7 @@ export function MobilePurchaseRequestDetailPage() {
|
||||
<div className="mobile-field-value">{formatDate(detail.expectedDeliveryDate)}</div>
|
||||
</div>
|
||||
<div className="mobile-field-row">
|
||||
<div className="mobile-field-label">法体</div>
|
||||
<div className="mobile-field-label">公司</div>
|
||||
<div className="mobile-field-value">{detail.legalEntity?.name ?? "-"}</div>
|
||||
</div>
|
||||
<div className="mobile-field-row">
|
||||
|
||||
@@ -49,7 +49,7 @@ function resolvePurchaseListNextStep(status: PurchaseRequestStatus) {
|
||||
|
||||
function resolvePurchaseListEvidence(item: PurchaseRequestSummary) {
|
||||
if (!item.supplierId) return "待补供应商";
|
||||
if (!item.legalEntityId || !item.costCenterId) return "待补法体 / 成本中心";
|
||||
if (!item.legalEntityId || !item.costCenterId) return "待补公司 / 成本中心";
|
||||
if (!item.documentCategoryId) return "待补单据分类";
|
||||
if (!item.budgetPoolId) return "预算池未匹配";
|
||||
if (!item.projectId) return "未关联项目";
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { toast } from "sonner";
|
||||
|
||||
@@ -14,8 +15,9 @@ import {
|
||||
import { fetchLegalEntities } from "../api/masterData";
|
||||
import { isApiError } from "../api/types";
|
||||
import { useAuth } from "../auth/AuthContext";
|
||||
import { hasPermission } from "../auth/permissions";
|
||||
import { hasAnyPermission, hasPermission } from "../auth/permissions";
|
||||
import { PageHeader } from "../components/common/PageHeader";
|
||||
import { resolveSingleCompanyState } from "../features/company/singleCompany";
|
||||
import { PageScaffold } from "../components/patterns/PageScaffold";
|
||||
import { SectionPanel } from "../components/patterns/SectionPanel";
|
||||
import { Button } from "../components/ui/button";
|
||||
@@ -33,6 +35,7 @@ export function PaymentAccountsPage() {
|
||||
const { permissions } = useAuth();
|
||||
const canManage = hasPermission(permissions, "payment.account.manage");
|
||||
const canReveal = hasPermission(permissions, "payment.account.read_full");
|
||||
const canManageCompanyInformation = hasAnyPermission(permissions, ["masterdata.view", "masterdata.manage"]);
|
||||
const [editing, setEditing] = useState<PaymentAccount | null>(null);
|
||||
const [legalEntityId, setLegalEntityId] = useState("");
|
||||
const [bankInstitutionId, setBankInstitutionId] = useState("");
|
||||
@@ -51,9 +54,19 @@ export function PaymentAccountsPage() {
|
||||
enabled: canManage,
|
||||
});
|
||||
|
||||
const activeCompanies = legalEntitiesQuery.data?.items ?? [];
|
||||
const companyState = resolveSingleCompanyState(activeCompanies);
|
||||
const singleCompany = companyState.kind === "single" ? companyState.company : null;
|
||||
|
||||
useEffect(() => {
|
||||
if (!editing && singleCompany) {
|
||||
setLegalEntityId(singleCompany.id);
|
||||
}
|
||||
}, [editing, singleCompany]);
|
||||
|
||||
const resetForm = () => {
|
||||
setEditing(null);
|
||||
setLegalEntityId("");
|
||||
setLegalEntityId(singleCompany?.id ?? "");
|
||||
setBankInstitutionId("");
|
||||
setAccountName("");
|
||||
setAccountNumber("");
|
||||
@@ -93,17 +106,56 @@ export function PaymentAccountsPage() {
|
||||
const accounts = accountsQuery.data?.items ?? [];
|
||||
return (
|
||||
<PageScaffold>
|
||||
<PageHeader title="企业银行账户" subtitle="维护法人主体的付款来源账户;完整账号仅在填写原因后临时显示。" />
|
||||
<PageHeader title="企业银行账户" subtitle="维护当前公司的付款来源账户;完整账号仅在填写原因后临时显示。" />
|
||||
<div className="mx-auto max-w-6xl space-y-5">
|
||||
{canManage ? (
|
||||
{canManage && legalEntitiesQuery.isLoading ? (
|
||||
<SectionPanel title="企业银行账户">
|
||||
<div className="text-sm text-zinc-500">正在读取公司信息...</div>
|
||||
</SectionPanel>
|
||||
) : null}
|
||||
|
||||
{canManage && legalEntitiesQuery.isError ? (
|
||||
<SectionPanel title="企业银行账户">
|
||||
<div className="text-sm text-red-600">公司信息读取失败,请刷新后重试。</div>
|
||||
</SectionPanel>
|
||||
) : null}
|
||||
|
||||
{canManage && !legalEntitiesQuery.isLoading && !legalEntitiesQuery.isError && companyState.kind === "empty" ? (
|
||||
<SectionPanel title="先完善公司信息">
|
||||
<div className="space-y-3 text-sm text-zinc-600">
|
||||
<p>企业银行账户必须关联当前公司。请先完善公司信息,再回来录入付款账户。</p>
|
||||
{canManageCompanyInformation ? (
|
||||
<Button asChild type="button" variant="secondary">
|
||||
<Link to="/system/master-data">完善公司信息</Link>
|
||||
</Button>
|
||||
) : (
|
||||
<p>请联系管理员先完善公司信息。</p>
|
||||
)}
|
||||
</div>
|
||||
</SectionPanel>
|
||||
) : null}
|
||||
|
||||
{canManage && companyState.kind === "multiple" ? (
|
||||
<SectionPanel title="公司信息需要处理">
|
||||
<div className="space-y-3 text-sm text-red-600">
|
||||
<p>检测到多条启用的公司信息,系统不会猜测付款账户应关联哪一条。请先处理公司信息。</p>
|
||||
{canManageCompanyInformation ? (
|
||||
<Button asChild type="button" variant="secondary">
|
||||
<Link to="/system/master-data">查看公司信息</Link>
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
</SectionPanel>
|
||||
) : null}
|
||||
|
||||
{canManage && singleCompany ? (
|
||||
<SectionPanel title={editing ? "替换企业银行账户" : "新增企业银行账户"}>
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="payment-account-legal-entity">法人主体</Label>
|
||||
<select id="payment-account-legal-entity" className="select-std w-full" value={legalEntityId} disabled={Boolean(editing)} onChange={(event) => setLegalEntityId(event.target.value)}>
|
||||
<option value="">请选择</option>
|
||||
{(legalEntitiesQuery.data?.items ?? []).map((item) => <option key={item.id} value={item.id}>{item.name}</option>)}
|
||||
</select>
|
||||
<Label>当前公司</Label>
|
||||
<div className="input-std flex min-h-10 items-center bg-zinc-50 text-sm text-zinc-700" data-testid="payment-account-company">
|
||||
{editing?.legalEntity.name ?? singleCompany.name}
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="payment-account-bank">开户银行</Label>
|
||||
@@ -158,7 +210,7 @@ export function PaymentAccountsPage() {
|
||||
</Dialog>
|
||||
<Dialog open={Boolean(disableTarget)} onOpenChange={(open) => { if (!open) setDisableTarget(null); }}>
|
||||
<DialogContent className="max-w-md p-5">
|
||||
<DialogHeader><DialogTitle>停用企业银行账户</DialogTitle><DialogDescription>停用后不能为新付款批次生成制单;历史批次仍保留冻结账户摘要。确认停用 {disableTarget?.legalEntity.name ?? "该法人主体"} 的当前账户吗?</DialogDescription></DialogHeader>
|
||||
<DialogHeader><DialogTitle>停用企业银行账户</DialogTitle><DialogDescription>停用后不能为新付款批次生成制单;历史批次仍保留冻结账户摘要。确认停用 {disableTarget?.legalEntity.name ?? "当前公司"} 的当前账户吗?</DialogDescription></DialogHeader>
|
||||
<DialogFooter><Button type="button" variant="secondary" onClick={() => setDisableTarget(null)}>返回</Button><Button type="button" variant="destructive" disabled={disableMutation.isPending} onClick={() => { if (disableTarget) disableMutation.mutate(disableTarget.id); }}>{disableMutation.isPending ? "停用中..." : "确认停用"}</Button></DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
@@ -178,7 +178,7 @@ function resolvePurchaseEvidenceHint(item: {
|
||||
}) {
|
||||
const missingFields = [
|
||||
item.supplierId ? null : "供应商",
|
||||
item.legalEntityId ? null : "法体",
|
||||
item.legalEntityId ? null : "公司",
|
||||
item.costCenterId ? null : "成本中心",
|
||||
item.documentCategoryId ? null : "单据分类",
|
||||
].filter(Boolean);
|
||||
@@ -590,7 +590,7 @@ export function PurchaseRequestsPage() {
|
||||
throw new Error("提交前请选择供应商");
|
||||
}
|
||||
if (!legalEntityId) {
|
||||
throw new Error("提交前请选择法体");
|
||||
throw new Error("提交前请选择公司");
|
||||
}
|
||||
if (!costCenterId) {
|
||||
throw new Error("提交前请选择成本中心");
|
||||
@@ -991,7 +991,7 @@ export function PurchaseRequestsPage() {
|
||||
setPage(1);
|
||||
}}
|
||||
>
|
||||
<option value="">全部法体</option>
|
||||
<option value="">全部公司</option>
|
||||
{(legalEntitiesQuery.data?.items ?? []).map((item) => (
|
||||
<option key={item.id} value={item.id}>
|
||||
{item.name}
|
||||
@@ -1344,7 +1344,7 @@ export function PurchaseRequestsPage() {
|
||||
<Input value={currencyCode} onChange={(event) => setCurrencyCode(event.target.value.toUpperCase())} disabled={!canEditDraft} />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-zinc-700">法体</label>
|
||||
<label className="text-sm font-medium text-zinc-700">公司</label>
|
||||
<select
|
||||
data-testid="purchase-legal-entity"
|
||||
className="select-std"
|
||||
@@ -1358,7 +1358,7 @@ export function PurchaseRequestsPage() {
|
||||
}
|
||||
}}
|
||||
>
|
||||
<option value="">请选择法体</option>
|
||||
<option value="">请选择公司</option>
|
||||
{(legalEntitiesQuery.data?.items ?? []).map((item) => (
|
||||
<option key={item.id} value={item.id}>
|
||||
{item.name}
|
||||
|
||||
@@ -1,40 +1,12 @@
|
||||
import { Link } from "react-router-dom";
|
||||
import { useAuth } from "../auth/AuthContext";
|
||||
import { hasAnyPermission } from "../auth/permissions";
|
||||
import { ListWorkspace, WorkspaceHeader } from "../components/patterns/ListWorkspace";
|
||||
|
||||
type ManagementItem = {
|
||||
to: string;
|
||||
icon: string;
|
||||
title: string;
|
||||
description: string;
|
||||
group: "人员与组织" | "流程与规则" | "系统与审计" | "功能管理";
|
||||
anyOf: string[];
|
||||
};
|
||||
|
||||
const ITEMS: ManagementItem[] = [
|
||||
{ to: "/people", icon: "badge", title: "人员", description: "员工档案、入转调离和人员状态。", group: "人员与组织", anyOf: ["system.users"] },
|
||||
{ to: "/organization", icon: "account_tree", title: "组织", description: "组织结构、部门和汇报关系。", group: "人员与组织", anyOf: ["system.users", "report.dashboard.view"] },
|
||||
{ to: "/system/users", icon: "manage_accounts", title: "账号", description: "账号、角色和登录权限。", group: "人员与组织", anyOf: ["system.accounts.manage", "system.roles.manage"] },
|
||||
{ to: "/system/departments", icon: "account_tree", title: "部门", description: "部门基础数据维护。", group: "人员与组织", anyOf: ["system.users"] },
|
||||
{ to: "/system/approval-rules", icon: "rule", title: "审批规则", description: "金额段、审批层级和处理规则。", group: "流程与规则", anyOf: ["system.settings"] },
|
||||
{ to: "/system/workflow-definitions", icon: "schema", title: "流程定义", description: "流程模板和节点治理。", group: "流程与规则", anyOf: ["system.settings"] },
|
||||
{ to: "/system/expense-standards", icon: "rule_settings", title: "费用标准", description: "费用上限、类别和管控口径。", group: "流程与规则", anyOf: ["system.settings"] },
|
||||
{ to: "/system/reimbursement-templates", icon: "receipt_long", title: "报销模板", description: "常见报销场景模板。", group: "流程与规则", anyOf: ["system.settings"] },
|
||||
{ to: "/system/workspace", icon: "dashboard_customize", title: "工作区", description: "菜单入口、模块显隐和小团队基线。", group: "系统与审计", anyOf: ["system.settings"] },
|
||||
{ to: "/audit-logs", icon: "manage_search", title: "审计日志", description: "关键操作留痕和排障。", group: "系统与审计", anyOf: ["system.audit.view"] },
|
||||
{ to: "/system/master-data", icon: "dataset", title: "主数据", description: "法体、成本中心、供应商等基础数据。", group: "功能管理", anyOf: ["masterdata.view", "masterdata.manage"] },
|
||||
{ to: "/system/payment-accounts", icon: "account_balance", title: "企业银行账户", description: "维护付款来源账户、脱敏查看与变更留痕。", group: "功能管理", anyOf: ["payment.account.read", "payment.account.manage"] },
|
||||
{ to: "/system/notification-channels", icon: "notifications_active", title: "通知渠道", description: "企业微信、邮件等补充通知。", group: "功能管理", anyOf: ["notification.channel.manage"] },
|
||||
{ to: "/system/feature-flags", icon: "toggle_on", title: "功能管理", description: "功能开关、灰度和试运行边界。", group: "功能管理", anyOf: ["system.settings"] },
|
||||
];
|
||||
|
||||
const GROUPS: ManagementItem["group"][] = ["人员与组织", "流程与规则", "系统与审计", "功能管理"];
|
||||
import { getVisibleManagementItems, MANAGEMENT_GROUPS } from "../features/navigation/managementRoutes";
|
||||
|
||||
export function SystemSettingsPage() {
|
||||
const { permissions } = useAuth();
|
||||
const visibleItems = ITEMS.filter((item) => hasAnyPermission(permissions, item.anyOf));
|
||||
const visibleGroups = GROUPS.map((group) => ({
|
||||
const visibleItems = getVisibleManagementItems(permissions);
|
||||
const visibleGroups = MANAGEMENT_GROUPS.map((group) => ({
|
||||
group,
|
||||
items: visibleItems.filter((item) => item.group === group),
|
||||
})).filter((entry) => entry.items.length > 0);
|
||||
@@ -74,7 +46,7 @@ export function SystemSettingsPage() {
|
||||
<h2 className="text-base font-semibold text-zinc-900">{group}</h2>
|
||||
<div className="admin-tile-grid">
|
||||
{items.map((item) => (
|
||||
<Link key={item.to} to={item.to} className="admin-tile-card">
|
||||
<Link key={item.key} to={item.path} className="admin-tile-card">
|
||||
<div className="flex items-start gap-3">
|
||||
<span className="admin-tile-card__icon">
|
||||
<span className="material-symbols-outlined text-[20px]">{item.icon}</span>
|
||||
|
||||
@@ -407,7 +407,7 @@ export function TravelRequestsPage() {
|
||||
throw new Error("请选择出差起止日期");
|
||||
}
|
||||
if (!legalEntityId || !costCenterId) {
|
||||
throw new Error("请选择法体与成本中心");
|
||||
throw new Error("请选择公司与成本中心");
|
||||
}
|
||||
if (!estimatedAmount || Number(estimatedAmount) < 0) {
|
||||
throw new Error("请填写有效的预估金额");
|
||||
@@ -614,7 +614,7 @@ export function TravelRequestsPage() {
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="travel-filter-legal-entity">法体</Label>
|
||||
<Label htmlFor="travel-filter-legal-entity">公司</Label>
|
||||
<select
|
||||
id="travel-filter-legal-entity"
|
||||
className="select-std mt-1"
|
||||
@@ -625,7 +625,7 @@ export function TravelRequestsPage() {
|
||||
setPage(1);
|
||||
}}
|
||||
>
|
||||
<option value="">全部法体</option>
|
||||
<option value="">全部公司</option>
|
||||
{(legalEntitiesQuery.data?.items ?? []).map((item) => (
|
||||
<option key={item.id} value={item.id}>{item.name}</option>
|
||||
))}
|
||||
@@ -828,7 +828,7 @@ export function TravelRequestsPage() {
|
||||
<p className="font-medium text-zinc-900">{detail.budgetPool?.name ?? "待提交时自动解析"}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-zinc-500">法体</p>
|
||||
<p className="text-zinc-500">公司</p>
|
||||
<p className="font-medium text-zinc-900">{detail.legalEntity?.name ?? "-"}</p>
|
||||
</div>
|
||||
<div>
|
||||
@@ -1030,7 +1030,7 @@ function TravelRequestForm(props: TravelRequestFormProps) {
|
||||
<Input id="travel-end-date" className="mt-1" type="date" value={endDate} onChange={(event) => setEndDate(event.target.value)} readOnly={readOnly} />
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="travel-legal-entity">法体</Label>
|
||||
<Label htmlFor="travel-legal-entity">公司</Label>
|
||||
<select
|
||||
id="travel-legal-entity"
|
||||
className="select-std mt-1"
|
||||
@@ -1041,7 +1041,7 @@ function TravelRequestForm(props: TravelRequestFormProps) {
|
||||
}}
|
||||
disabled={readOnly}
|
||||
>
|
||||
<option value="">请选择法体</option>
|
||||
<option value="">请选择公司</option>
|
||||
{legalEntities.map((item) => (
|
||||
<option key={item.id} value={item.id}>{item.name}</option>
|
||||
))}
|
||||
|
||||
@@ -351,7 +351,7 @@ export function WorkflowTaskDetailPage() {
|
||||
<DetailField label="标题" value={detail.purchaseRequest.title || "-"} />
|
||||
<DetailField label="申请人" value={detail.purchaseRequest.applicant?.name ?? "-"} />
|
||||
<DetailField label="供应商" value={detail.purchaseRequest.supplier?.name ?? "-"} />
|
||||
<DetailField label="法体" value={detail.purchaseRequest.legalEntity?.name ?? "-"} />
|
||||
<DetailField label="公司" value={detail.purchaseRequest.legalEntity?.name ?? "-"} />
|
||||
<DetailField label="成本中心" value={detail.purchaseRequest.costCenter?.name ?? "-"} />
|
||||
<DetailField label="单据分类" value={detail.purchaseRequest.documentCategory?.name ?? "-"} />
|
||||
<DetailField label="金额" value={`${detail.purchaseRequest.totalAmount ?? "-"} ${detail.purchaseRequest.currencyCode}`} />
|
||||
@@ -364,7 +364,7 @@ export function WorkflowTaskDetailPage() {
|
||||
<DetailField label="差旅单号" value={detail.travelRequest.requestNo ?? "暂未生成"} />
|
||||
<DetailField label="申请人" value={detail.travelRequest.applicant?.name ?? "-"} />
|
||||
<DetailField label="出差类型" value={detail.travelRequest.tripType === "international" ? "国际" : "国内"} />
|
||||
<DetailField label="法体" value={detail.travelRequest.legalEntity?.name ?? "-"} />
|
||||
<DetailField label="公司" value={detail.travelRequest.legalEntity?.name ?? "-"} />
|
||||
<DetailField label="成本中心" value={detail.travelRequest.costCenter?.name ?? "-"} />
|
||||
<DetailField label="起止日期" value={`${detail.travelRequest.startDate ?? "-"} → ${detail.travelRequest.endDate ?? "-"}`} />
|
||||
<DetailField label="预估金额" value={detail.travelRequest.estimatedAmount ?? "-"} />
|
||||
|
||||
在新工单中引用
屏蔽一个用户