feat(G2-4): 升级预算页为多作用域模式
这个提交包含在:
+32
-6
@@ -1,10 +1,30 @@
|
|||||||
import { apiRequestData } from "./http";
|
import { apiRequestData } from "./http";
|
||||||
|
|
||||||
|
export type BudgetScopeType = "global" | "department_legacy" | "cost_center_scoped" | "invalid";
|
||||||
|
|
||||||
export type BudgetPool = {
|
export type BudgetPool = {
|
||||||
id: string;
|
id: string;
|
||||||
code: string;
|
code: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
scopeType: BudgetScopeType;
|
||||||
departmentId: string | null;
|
departmentId: string | null;
|
||||||
|
legalEntityId: string | null;
|
||||||
|
costCenterId: string | null;
|
||||||
|
department: {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
} | null;
|
||||||
|
legalEntity: {
|
||||||
|
id: string;
|
||||||
|
code: string;
|
||||||
|
name: string;
|
||||||
|
} | null;
|
||||||
|
costCenter: {
|
||||||
|
id: string;
|
||||||
|
code: string;
|
||||||
|
name: string;
|
||||||
|
legalEntityId: string;
|
||||||
|
} | null;
|
||||||
status: "active" | "archived";
|
status: "active" | "archived";
|
||||||
periodStart: string | null;
|
periodStart: string | null;
|
||||||
periodEnd: string | null;
|
periodEnd: string | null;
|
||||||
@@ -27,28 +47,34 @@ export type BudgetListResponse = {
|
|||||||
export type BudgetListQuery = {
|
export type BudgetListQuery = {
|
||||||
status?: "active" | "archived";
|
status?: "active" | "archived";
|
||||||
departmentId?: string;
|
departmentId?: string;
|
||||||
|
legalEntityId?: string;
|
||||||
|
costCenterId?: string;
|
||||||
page?: number;
|
page?: number;
|
||||||
pageSize?: number;
|
pageSize?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
function toQueryString(query: BudgetListQuery = {}) {
|
function toQueryString(query: Record<string, string | number | undefined>) {
|
||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
if (query.status) params.set("status", query.status);
|
for (const [key, value] of Object.entries(query)) {
|
||||||
if (query.departmentId) params.set("departmentId", query.departmentId);
|
if (value == null || value === "") {
|
||||||
if (query.page != null) params.set("page", String(query.page));
|
continue;
|
||||||
if (query.pageSize != null) params.set("pageSize", String(query.pageSize));
|
}
|
||||||
|
params.set(key, String(value));
|
||||||
|
}
|
||||||
const encoded = params.toString();
|
const encoded = params.toString();
|
||||||
return encoded ? `?${encoded}` : "";
|
return encoded ? `?${encoded}` : "";
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchBudgets(query: BudgetListQuery = {}) {
|
export async function fetchBudgets(query: BudgetListQuery = {}) {
|
||||||
return apiRequestData<BudgetListResponse>(`/api/budgets${toQueryString(query)}`);
|
return apiRequestData<BudgetListResponse>(`/api/budgets${toQueryString(query as Record<string, string | number | undefined>)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createBudget(payload: {
|
export async function createBudget(payload: {
|
||||||
code: string;
|
code: string;
|
||||||
name: string;
|
name: string;
|
||||||
departmentId?: string | null;
|
departmentId?: string | null;
|
||||||
|
legalEntityId?: string | null;
|
||||||
|
costCenterId?: string | null;
|
||||||
periodStart: string;
|
periodStart: string;
|
||||||
periodEnd: string;
|
periodEnd: string;
|
||||||
allocatedAmount: string;
|
allocatedAmount: string;
|
||||||
|
|||||||
+200
-30
@@ -1,8 +1,9 @@
|
|||||||
import { useMemo, useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { mapApiErrorMessage } from "../api/errorMessages";
|
import { mapApiErrorMessage } from "../api/errorMessages";
|
||||||
import { createBudget, fetchBudgets, archiveBudget, type BudgetListQuery } from "../api/budgets";
|
import { archiveBudget, createBudget, fetchBudgets, type BudgetListQuery, type BudgetPool, type BudgetScopeType } from "../api/budgets";
|
||||||
import { fetchDepartments } from "../api/departments";
|
import { fetchDepartments } from "../api/departments";
|
||||||
|
import { fetchCostCenters, fetchLegalEntities } from "../api/masterData";
|
||||||
import { isApiError } from "../api/types";
|
import { isApiError } from "../api/types";
|
||||||
import { useAuth } from "../auth/AuthContext";
|
import { useAuth } from "../auth/AuthContext";
|
||||||
import { hasPermission } from "../auth/permissions";
|
import { hasPermission } from "../auth/permissions";
|
||||||
@@ -17,6 +18,8 @@ import { Pagination } from "../components/common/Pagination";
|
|||||||
|
|
||||||
const PAGE_SIZE = 20;
|
const PAGE_SIZE = 20;
|
||||||
|
|
||||||
|
type FormScopeType = Exclude<BudgetScopeType, "invalid">;
|
||||||
|
|
||||||
function resolveError(error: unknown) {
|
function resolveError(error: unknown) {
|
||||||
if (isApiError(error)) {
|
if (isApiError(error)) {
|
||||||
return `${error.code}: ${mapApiErrorMessage(error.code, error.details, error.message)}`;
|
return `${error.code}: ${mapApiErrorMessage(error.code, error.details, error.message)}`;
|
||||||
@@ -27,6 +30,24 @@ function resolveError(error: unknown) {
|
|||||||
return "请求失败,请稍后重试。";
|
return "请求失败,请稍后重试。";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function describeScope(item: BudgetPool) {
|
||||||
|
if (item.scopeType === "department_legacy") {
|
||||||
|
return item.department ? `部门:${item.department.name}` : "部门预算";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.scopeType === "cost_center_scoped") {
|
||||||
|
const legalEntityName = item.legalEntity?.name ?? "未知法体";
|
||||||
|
const costCenterName = item.costCenter?.name ?? "未知成本中心";
|
||||||
|
return `法体:${legalEntityName} / 成本中心:${costCenterName}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.scopeType === "global") {
|
||||||
|
return "全局预算";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "异常作用域";
|
||||||
|
}
|
||||||
|
|
||||||
export function BudgetsPage() {
|
export function BudgetsPage() {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const { permissions } = useAuth();
|
const { permissions } = useAuth();
|
||||||
@@ -36,8 +57,11 @@ export function BudgetsPage() {
|
|||||||
|
|
||||||
const [status, setStatus] = useState<"active" | "archived" | "">("active");
|
const [status, setStatus] = useState<"active" | "archived" | "">("active");
|
||||||
const [departmentId, setDepartmentId] = useState("");
|
const [departmentId, setDepartmentId] = useState("");
|
||||||
|
const [legalEntityId, setLegalEntityId] = useState("");
|
||||||
|
const [costCenterId, setCostCenterId] = useState("");
|
||||||
const [page, setPage] = useState(1);
|
const [page, setPage] = useState(1);
|
||||||
|
|
||||||
|
const [formScopeType, setFormScopeType] = useState<FormScopeType>("global");
|
||||||
const [code, setCode] = useState("");
|
const [code, setCode] = useState("");
|
||||||
const [name, setName] = useState("");
|
const [name, setName] = useState("");
|
||||||
const [periodStart, setPeriodStart] = useState("");
|
const [periodStart, setPeriodStart] = useState("");
|
||||||
@@ -45,6 +69,8 @@ export function BudgetsPage() {
|
|||||||
const [allocatedAmount, setAllocatedAmount] = useState("");
|
const [allocatedAmount, setAllocatedAmount] = useState("");
|
||||||
const [warningThresholdPercent, setWarningThresholdPercent] = useState("80");
|
const [warningThresholdPercent, setWarningThresholdPercent] = useState("80");
|
||||||
const [formDepartmentId, setFormDepartmentId] = useState("");
|
const [formDepartmentId, setFormDepartmentId] = useState("");
|
||||||
|
const [formLegalEntityId, setFormLegalEntityId] = useState("");
|
||||||
|
const [formCostCenterId, setFormCostCenterId] = useState("");
|
||||||
const [error, setError] = useState("");
|
const [error, setError] = useState("");
|
||||||
const [confirmArchiveId, setConfirmArchiveId] = useState<{ id: string; code: string } | null>(null);
|
const [confirmArchiveId, setConfirmArchiveId] = useState<{ id: string; code: string } | null>(null);
|
||||||
|
|
||||||
@@ -52,10 +78,12 @@ export function BudgetsPage() {
|
|||||||
() => ({
|
() => ({
|
||||||
status: status || undefined,
|
status: status || undefined,
|
||||||
departmentId: departmentId || undefined,
|
departmentId: departmentId || undefined,
|
||||||
|
legalEntityId: legalEntityId || undefined,
|
||||||
|
costCenterId: costCenterId || undefined,
|
||||||
page,
|
page,
|
||||||
pageSize: PAGE_SIZE,
|
pageSize: PAGE_SIZE,
|
||||||
}),
|
}),
|
||||||
[status, departmentId, page],
|
[status, departmentId, legalEntityId, costCenterId, page],
|
||||||
);
|
);
|
||||||
|
|
||||||
const departmentsQuery = useQuery({
|
const departmentsQuery = useQuery({
|
||||||
@@ -64,18 +92,46 @@ export function BudgetsPage() {
|
|||||||
queryFn: () => fetchDepartments(),
|
queryFn: () => fetchDepartments(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const legalEntitiesQuery = useQuery({
|
||||||
|
enabled: canViewBudget,
|
||||||
|
queryKey: ["legal-entities", "budget-filter"],
|
||||||
|
queryFn: () => fetchLegalEntities({ enabled: true }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const costCentersQuery = useQuery({
|
||||||
|
enabled: canViewBudget,
|
||||||
|
queryKey: ["cost-centers", "budget-filter"],
|
||||||
|
queryFn: () => fetchCostCenters({ enabled: true }),
|
||||||
|
});
|
||||||
|
|
||||||
const budgetsQuery = useQuery({
|
const budgetsQuery = useQuery({
|
||||||
enabled: canViewBudget,
|
enabled: canViewBudget,
|
||||||
queryKey: ["budgets", queryInput],
|
queryKey: ["budgets", queryInput],
|
||||||
queryFn: () => fetchBudgets(queryInput),
|
queryFn: () => fetchBudgets(queryInput),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const filterCostCenters = useMemo(() => {
|
||||||
|
if (!legalEntityId) {
|
||||||
|
return costCentersQuery.data?.items ?? [];
|
||||||
|
}
|
||||||
|
return (costCentersQuery.data?.items ?? []).filter((item) => item.legalEntityId === legalEntityId);
|
||||||
|
}, [costCentersQuery.data?.items, legalEntityId]);
|
||||||
|
|
||||||
|
const formCostCenters = useMemo(() => {
|
||||||
|
if (!formLegalEntityId) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return (costCentersQuery.data?.items ?? []).filter((item) => item.legalEntityId === formLegalEntityId);
|
||||||
|
}, [costCentersQuery.data?.items, formLegalEntityId]);
|
||||||
|
|
||||||
const createMutation = useMutation({
|
const createMutation = useMutation({
|
||||||
mutationFn: async () => {
|
mutationFn: async () => {
|
||||||
return createBudget({
|
return createBudget({
|
||||||
code: code.trim().toUpperCase(),
|
code: code.trim().toUpperCase(),
|
||||||
name: name.trim(),
|
name: name.trim(),
|
||||||
departmentId: formDepartmentId || null,
|
departmentId: formScopeType === "department_legacy" ? formDepartmentId || null : null,
|
||||||
|
legalEntityId: formScopeType === "cost_center_scoped" ? formLegalEntityId || null : null,
|
||||||
|
costCenterId: formScopeType === "cost_center_scoped" ? formCostCenterId || null : null,
|
||||||
periodStart,
|
periodStart,
|
||||||
periodEnd,
|
periodEnd,
|
||||||
allocatedAmount: allocatedAmount.trim(),
|
allocatedAmount: allocatedAmount.trim(),
|
||||||
@@ -91,6 +147,9 @@ export function BudgetsPage() {
|
|||||||
setAllocatedAmount("");
|
setAllocatedAmount("");
|
||||||
setWarningThresholdPercent("80");
|
setWarningThresholdPercent("80");
|
||||||
setFormDepartmentId("");
|
setFormDepartmentId("");
|
||||||
|
setFormLegalEntityId("");
|
||||||
|
setFormCostCenterId("");
|
||||||
|
setFormScopeType("global");
|
||||||
setError("");
|
setError("");
|
||||||
toast.success("预算池已创建");
|
toast.success("预算池已创建");
|
||||||
},
|
},
|
||||||
@@ -123,12 +182,21 @@ export function BudgetsPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const totalPages = Math.max(1, Math.ceil((budgetsQuery.data?.total ?? 0) / PAGE_SIZE));
|
const totalPages = Math.max(1, Math.ceil((budgetsQuery.data?.total ?? 0) / PAGE_SIZE));
|
||||||
|
const createDisabled =
|
||||||
|
createMutation.isPending ||
|
||||||
|
!code.trim() ||
|
||||||
|
!name.trim() ||
|
||||||
|
!periodStart ||
|
||||||
|
!periodEnd ||
|
||||||
|
!allocatedAmount.trim() ||
|
||||||
|
(formScopeType === "department_legacy" && !formDepartmentId) ||
|
||||||
|
(formScopeType === "cost_center_scoped" && (!formLegalEntityId || !formCostCenterId));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<PageHeader
|
<PageHeader
|
||||||
title="预算管理"
|
title="预算管理"
|
||||||
subtitle="支持预算池维护、状态筛选与归档。"
|
subtitle="支持全局预算、部门预算与法体/成本中心预算的统一维护。"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{error ? (
|
{error ? (
|
||||||
@@ -139,7 +207,7 @@ export function BudgetsPage() {
|
|||||||
|
|
||||||
<div className="card-std p-6 space-y-3">
|
<div className="card-std p-6 space-y-3">
|
||||||
<h3 className="text-base font-semibold text-zinc-900">筛选条件</h3>
|
<h3 className="text-base font-semibold text-zinc-900">筛选条件</h3>
|
||||||
<div className="grid gap-3 md:grid-cols-3">
|
<div className="grid gap-3 md:grid-cols-4">
|
||||||
<select
|
<select
|
||||||
className="select-std"
|
className="select-std"
|
||||||
value={status}
|
value={status}
|
||||||
@@ -161,7 +229,7 @@ export function BudgetsPage() {
|
|||||||
setPage(1);
|
setPage(1);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<option value="">全部部门</option>
|
<option value="">全部部门预算</option>
|
||||||
{(departmentsQuery.data?.items ?? []).map((department) => (
|
{(departmentsQuery.data?.items ?? []).map((department) => (
|
||||||
<option key={department.id} value={department.id}>
|
<option key={department.id} value={department.id}>
|
||||||
{department.name}
|
{department.name}
|
||||||
@@ -169,6 +237,44 @@ export function BudgetsPage() {
|
|||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
|
<select
|
||||||
|
className="select-std"
|
||||||
|
value={legalEntityId}
|
||||||
|
onChange={(event) => {
|
||||||
|
const nextLegalEntityId = event.target.value;
|
||||||
|
setLegalEntityId(nextLegalEntityId);
|
||||||
|
if (costCenterId && !filterCostCenters.some((item) => item.id === costCenterId && item.legalEntityId === nextLegalEntityId)) {
|
||||||
|
setCostCenterId("");
|
||||||
|
}
|
||||||
|
setPage(1);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<option value="">全部法体</option>
|
||||||
|
{(legalEntitiesQuery.data?.items ?? []).map((legalEntity) => (
|
||||||
|
<option key={legalEntity.id} value={legalEntity.id}>
|
||||||
|
{legalEntity.name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select
|
||||||
|
className="select-std"
|
||||||
|
value={costCenterId}
|
||||||
|
onChange={(event) => {
|
||||||
|
setCostCenterId(event.target.value);
|
||||||
|
setPage(1);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<option value="">全部成本中心</option>
|
||||||
|
{filterCostCenters.map((costCenter) => (
|
||||||
|
<option key={costCenter.id} value={costCenter.id}>
|
||||||
|
{costCenter.name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
@@ -176,6 +282,19 @@ export function BudgetsPage() {
|
|||||||
>
|
>
|
||||||
刷新列表
|
刷新列表
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => {
|
||||||
|
setStatus("active");
|
||||||
|
setDepartmentId("");
|
||||||
|
setLegalEntityId("");
|
||||||
|
setCostCenterId("");
|
||||||
|
setPage(1);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
重置筛选
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -197,16 +316,68 @@ export function BudgetsPage() {
|
|||||||
/>
|
/>
|
||||||
<select
|
<select
|
||||||
className="select-std"
|
className="select-std"
|
||||||
value={formDepartmentId}
|
value={formScopeType}
|
||||||
onChange={(event) => setFormDepartmentId(event.target.value)}
|
onChange={(event) => {
|
||||||
|
const nextScopeType = event.target.value as FormScopeType;
|
||||||
|
setFormScopeType(nextScopeType);
|
||||||
|
setFormDepartmentId("");
|
||||||
|
setFormLegalEntityId("");
|
||||||
|
setFormCostCenterId("");
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<option value="">全局预算(不限定部门)</option>
|
<option value="global">全局预算</option>
|
||||||
{(departmentsQuery.data?.items ?? []).map((department) => (
|
<option value="department_legacy">部门预算</option>
|
||||||
<option key={department.id} value={department.id}>
|
<option value="cost_center_scoped">法体 + 成本中心预算</option>
|
||||||
{department.name}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
|
{formScopeType === "department_legacy" ? (
|
||||||
|
<select
|
||||||
|
className="select-std"
|
||||||
|
value={formDepartmentId}
|
||||||
|
onChange={(event) => setFormDepartmentId(event.target.value)}
|
||||||
|
>
|
||||||
|
<option value="">选择部门</option>
|
||||||
|
{(departmentsQuery.data?.items ?? []).map((department) => (
|
||||||
|
<option key={department.id} value={department.id}>
|
||||||
|
{department.name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{formScopeType === "cost_center_scoped" ? (
|
||||||
|
<>
|
||||||
|
<select
|
||||||
|
className="select-std"
|
||||||
|
value={formLegalEntityId}
|
||||||
|
onChange={(event) => {
|
||||||
|
setFormLegalEntityId(event.target.value);
|
||||||
|
setFormCostCenterId("");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<option value="">选择法体</option>
|
||||||
|
{(legalEntitiesQuery.data?.items ?? []).map((legalEntity) => (
|
||||||
|
<option key={legalEntity.id} value={legalEntity.id}>
|
||||||
|
{legalEntity.name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
<select
|
||||||
|
className="select-std"
|
||||||
|
value={formCostCenterId}
|
||||||
|
onChange={(event) => setFormCostCenterId(event.target.value)}
|
||||||
|
disabled={!formLegalEntityId}
|
||||||
|
>
|
||||||
|
<option value="">选择成本中心</option>
|
||||||
|
{formCostCenters.map((costCenter) => (
|
||||||
|
<option key={costCenter.id} value={costCenter.id}>
|
||||||
|
{costCenter.name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
|
||||||
<input
|
<input
|
||||||
className="input-std"
|
className="input-std"
|
||||||
type="date"
|
type="date"
|
||||||
@@ -235,18 +406,7 @@ export function BudgetsPage() {
|
|||||||
onChange={(event) => setWarningThresholdPercent(event.target.value)}
|
onChange={(event) => setWarningThresholdPercent(event.target.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button type="button" disabled={createDisabled} onClick={() => createMutation.mutate()}>
|
||||||
type="button"
|
|
||||||
disabled={
|
|
||||||
createMutation.isPending ||
|
|
||||||
!code.trim() ||
|
|
||||||
!name.trim() ||
|
|
||||||
!periodStart ||
|
|
||||||
!periodEnd ||
|
|
||||||
!allocatedAmount.trim()
|
|
||||||
}
|
|
||||||
onClick={() => createMutation.mutate()}
|
|
||||||
>
|
|
||||||
{createMutation.isPending ? "创建中..." : "创建预算池"}
|
{createMutation.isPending ? "创建中..." : "创建预算池"}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -255,7 +415,7 @@ export function BudgetsPage() {
|
|||||||
<div className="card-std p-6 space-y-3">
|
<div className="card-std p-6 space-y-3">
|
||||||
<h3 className="text-base font-semibold text-zinc-900">预算池列表</h3>
|
<h3 className="text-base font-semibold text-zinc-900">预算池列表</h3>
|
||||||
|
|
||||||
{budgetsQuery.isLoading ? <TableSkeleton columns={7} rows={4} /> : null}
|
{budgetsQuery.isLoading ? <TableSkeleton columns={8} rows={4} /> : null}
|
||||||
|
|
||||||
{!budgetsQuery.isLoading ? (
|
{!budgetsQuery.isLoading ? (
|
||||||
<>
|
<>
|
||||||
@@ -265,6 +425,7 @@ export function BudgetsPage() {
|
|||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead>编码</TableHead>
|
<TableHead>编码</TableHead>
|
||||||
<TableHead>名称</TableHead>
|
<TableHead>名称</TableHead>
|
||||||
|
<TableHead>作用域</TableHead>
|
||||||
<TableHead>状态</TableHead>
|
<TableHead>状态</TableHead>
|
||||||
<TableHead>期间</TableHead>
|
<TableHead>期间</TableHead>
|
||||||
<TableHead>额度</TableHead>
|
<TableHead>额度</TableHead>
|
||||||
@@ -278,6 +439,9 @@ export function BudgetsPage() {
|
|||||||
<TableRow key={item.id}>
|
<TableRow key={item.id}>
|
||||||
<TableCell className="font-medium text-zinc-900">{item.code}</TableCell>
|
<TableCell className="font-medium text-zinc-900">{item.code}</TableCell>
|
||||||
<TableCell>{item.name}</TableCell>
|
<TableCell>{item.name}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<div className="text-sm text-zinc-700">{describeScope(item)}</div>
|
||||||
|
</TableCell>
|
||||||
<TableCell><StatusBadge status={item.status} /></TableCell>
|
<TableCell><StatusBadge status={item.status} /></TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
{(item.periodStart ?? "-")} ~ {(item.periodEnd ?? "-")}
|
{(item.periodStart ?? "-")} ~ {(item.periodEnd ?? "-")}
|
||||||
@@ -306,7 +470,7 @@ export function BudgetsPage() {
|
|||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell className="py-3 text-zinc-500" colSpan={7}>
|
<TableCell className="py-3 text-zinc-500" colSpan={8}>
|
||||||
暂无预算池
|
暂无预算池
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
@@ -326,9 +490,15 @@ export function BudgetsPage() {
|
|||||||
description={`确认归档预算池 ${confirmArchiveId?.code ?? ""} ?`}
|
description={`确认归档预算池 ${confirmArchiveId?.code ?? ""} ?`}
|
||||||
variant="destructive"
|
variant="destructive"
|
||||||
onConfirm={async () => {
|
onConfirm={async () => {
|
||||||
if (confirmArchiveId) archiveMutation.mutate(confirmArchiveId.id);
|
if (confirmArchiveId) {
|
||||||
|
archiveMutation.mutate(confirmArchiveId.id);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
if (!open) {
|
||||||
|
setConfirmArchiveId(null);
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
onOpenChange={(open) => { if (!open) setConfirmArchiveId(null); }}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
在新工单中引用
屏蔽一个用户