imeeting/frontend/src/pages/business/TenantMeetingPointsSettings...

368 lines
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

import { ReloadOutlined, SearchOutlined } from "@ant-design/icons";
import { getCurrentUser } from "@/api";
import AppPagination from "@/components/shared/AppPagination";
import DataListPanel from "@/components/shared/DataListPanel";
import ListTable from "@/components/shared/ListTable/ListTable";
import PageContainer from "@/components/shared/PageContainer";
import SectionCard from "@/components/shared/SectionCard";
import {usePermission} from "@/hooks/usePermission";
import {
getCurrentTenantMeetingPointsSetting,
pageTenantMeetingPointsSettings,
type TenantMeetingPointsSettingVO,
updateTenantMeetingPointsBalanceCheck,
} from "@/api/business/meetingPoints";
import type { UserProfile } from "@/types";
import { Button, Card, Input, message, Modal, Select, Space, Statistic, Tag, Typography } from "antd";
import { useEffect, useState } from "react";
import "./TenantMeetingPointsSettings.css";
const { Text } = Typography;
const BALANCE_CHECK_UPDATE_PERMISSION = "biz:tenant-meeting-points:balance-check:update";
function formatDateTime(value?: string) {
return value ? value.replace("T", " ").substring(0, 19) : "-";
}
function renderStatusTag(enabled?: boolean) {
return enabled === false
? <Tag color="volcano"></Tag>
: <Tag color="green"></Tag>;
}
export default function TenantMeetingPointsSettings() {
const {can} = usePermission();
const [profile, setProfile] = useState<UserProfile | null>(null);
const [loading, setLoading] = useState(false);
const [switchingTenantId, setSwitchingTenantId] = useState<number | null>(null);
const [records, setRecords] = useState<TenantMeetingPointsSettingVO[]>([]);
const [total, setTotal] = useState(0);
const [currentTenantSetting, setCurrentTenantSetting] = useState<TenantMeetingPointsSettingVO | null>(null);
const [params, setParams] = useState({
current: 1,
size: 10,
tenantName: "",
tenantCode: "",
balanceCheckEnabled: "",
});
const isPlatformAdmin = Boolean(profile?.isPlatformAdmin);
const isTenantAdmin = Boolean(profile?.isTenantAdmin);
const canUpdateBalanceCheck = isPlatformAdmin || (isTenantAdmin && can(BALANCE_CHECK_UPDATE_PERMISSION));
const loadPlatformPage = async (nextParams = params) => {
setLoading(true);
try {
const result = await pageTenantMeetingPointsSettings({
current: nextParams.current,
size: nextParams.size,
tenantName: nextParams.tenantName || undefined,
tenantCode: nextParams.tenantCode || undefined,
balanceCheckEnabled: nextParams.balanceCheckEnabled === "" ? undefined : nextParams.balanceCheckEnabled === "true",
});
setRecords(result.records || []);
setTotal(result.total || 0);
} finally {
setLoading(false);
}
};
const loadCurrentTenant = async () => {
setLoading(true);
try {
const data = await getCurrentTenantMeetingPointsSetting();
setCurrentTenantSetting(data);
} finally {
setLoading(false);
}
};
const loadData = async () => {
const nextProfile = await getCurrentUser();
setProfile(nextProfile);
if (nextProfile.isPlatformAdmin) {
await loadPlatformPage();
return;
}
if (nextProfile.isTenantAdmin) {
await loadCurrentTenant();
return;
}
setCurrentTenantSetting(null);
setRecords([]);
setTotal(0);
};
useEffect(() => {
void loadData();
}, []);
const handleRefresh = async () => {
if (isPlatformAdmin) {
await loadPlatformPage();
} else if (isTenantAdmin) {
await loadCurrentTenant();
}
message.success("已刷新租户积分校验配置");
};
const confirmSwitch = (record: TenantMeetingPointsSettingVO, nextEnabled: boolean) => {
Modal.confirm({
title: nextEnabled ? "开启余额校验" : "关闭余额校验",
content: nextEnabled
? "开启后将按当前账面余额重新执行后续会议提交校验。"
: "关闭后将进入无限余额模式,只记录消耗和流水,不扣减账面余额。",
okText: "确认",
cancelText: "取消",
onOk: async () => {
setSwitchingTenantId(record.tenantId);
try {
await updateTenantMeetingPointsBalanceCheck(record.tenantId, { balanceCheckEnabled: nextEnabled });
message.success("余额校验开关已更新");
if (isPlatformAdmin) {
await loadPlatformPage();
} else {
await loadCurrentTenant();
}
} finally {
setSwitchingTenantId(null);
}
},
});
};
const columns = [
{
title: "租户名称",
dataIndex: "tenantName",
key: "tenantName",
width: 180,
render: (value: string) => <Text strong>{value || "-"}</Text>,
},
{
title: "租户编码",
dataIndex: "tenantCode",
key: "tenantCode",
width: 160,
render: (value: string) => <Text>{value || "-"}</Text>,
},
{
title: "余额校验状态",
dataIndex: "balanceCheckEnabled",
key: "balanceCheckEnabled",
width: 160,
render: (value: boolean) => renderStatusTag(value),
},
{
title: "公共账户余额",
dataIndex: "publicBalance",
key: "publicBalance",
width: 140,
render: (value: number) => value ?? 0,
},
{
title: "公共账户累计消耗",
dataIndex: "publicTotalPointsUsed",
key: "publicTotalPointsUsed",
width: 160,
render: (value: number) => value ?? 0,
},
{
title: "最近切换时间",
dataIndex: "lastSwitchAt",
key: "lastSwitchAt",
width: 180,
render: (value: string) => formatDateTime(value),
},
{
title: "最近切换人",
dataIndex: "lastSwitchByName",
key: "lastSwitchByName",
width: 160,
render: (value: string) => value || "-",
},
{
title: "操作",
key: "action",
width: 140,
fixed: "right" as const,
render: (_: unknown, record: TenantMeetingPointsSettingVO) => {
if (!canUpdateBalanceCheck) {
return null;
}
return (
<Button
type="link"
loading={switchingTenantId === record.tenantId}
onClick={() => confirmSwitch(record, !record.balanceCheckEnabled)}
>
{record.balanceCheckEnabled ? "切换为无限余额" : "开启余额校验"}
</Button>
);
},
},
];
const renderTenantAdminCard = () => {
if (!currentTenantSetting) {
return null;
}
return (
<SectionCard
title="当前租户"
description="租户管理员可查看并切换当前租户的积分余额校验模式。"
layout="auto"
>
<Space direction="vertical" size="large" className="tenant-meeting-points__tenant-card">
<div className="tenant-meeting-points__tenant-heading">
<Space direction="vertical" size={4}>
<Text strong style={{ fontSize: 18 }}>{currentTenantSetting.tenantName || "当前租户"}</Text>
<Text type="secondary">{currentTenantSetting.tenantCode || "-"}</Text>
</Space>
{renderStatusTag(currentTenantSetting.balanceCheckEnabled)}
</div>
<Card
size="small"
className={
currentTenantSetting.balanceCheckEnabled
? "tenant-meeting-points__mode-card tenant-meeting-points__mode-card--enabled"
: "tenant-meeting-points__mode-card tenant-meeting-points__mode-card--unlimited"
}
>
<Space direction="vertical" size={4}>
<Text strong>
{currentTenantSetting.balanceCheckEnabled ? "当前为校验余额模式" : "当前为无限余额模式"}
</Text>
<Text type="secondary">
{currentTenantSetting.balanceCheckEnabled
? "后续会议提交将按当前账面余额执行拦截与扣减。"
: "后续会议只记录消耗与流水,不扣减账面余额,积分分配也会被禁用。"}
</Text>
</Space>
</Card>
<Space size={40} wrap className="tenant-meeting-points__stats">
<Statistic title="当前可用额度" value={currentTenantSetting.balanceCheckEnabled ? currentTenantSetting.publicBalance ?? 0 : "无限"} />
<Statistic title="公共账户余额" value={currentTenantSetting.publicBalance ?? 0} />
<Statistic title="公共账户累计消耗" value={currentTenantSetting.publicTotalPointsUsed ?? 0} />
</Space>
<Space direction="vertical" size={4}>
<Text type="secondary">{formatDateTime(currentTenantSetting.lastSwitchAt)}</Text>
<Text type="secondary">{currentTenantSetting.lastSwitchByName || "-"}</Text>
</Space>
<Space>
{canUpdateBalanceCheck ? (
<Button
type="primary"
loading={switchingTenantId === currentTenantSetting.tenantId}
onClick={() => confirmSwitch(currentTenantSetting, !currentTenantSetting.balanceCheckEnabled)}
>
{currentTenantSetting.balanceCheckEnabled ? "切换为无限余额" : "开启余额校验"}
</Button>
) : null}
<Button icon={<ReloadOutlined />} onClick={() => void handleRefresh()}>
</Button>
</Space>
</Space>
</SectionCard>
);
};
return (
<PageContainer
title={null}
className="tenant-meeting-points"
>
{isPlatformAdmin ? (
<SectionCard
title="租户积分校验"
description="按租户控制会议积分是否校验余额,关闭后按无限余额模式记录消耗。"
>
<DataListPanel
leftActions={
<Button icon={<ReloadOutlined />} onClick={() => void handleRefresh()}>
</Button>
}
rightActions={
<Space wrap>
<Input
placeholder="按租户名称搜索"
value={params.tenantName}
onChange={(event) => setParams((prev) => ({ ...prev, tenantName: event.target.value }))}
style={{ width: 220 }}
prefix={<SearchOutlined className="text-gray-400" />}
allowClear
/>
<Input
placeholder="按租户编码搜索"
value={params.tenantCode}
onChange={(event) => setParams((prev) => ({ ...prev, tenantCode: event.target.value }))}
style={{ width: 180 }}
allowClear
/>
<Select
style={{ width: 180 }}
value={params.balanceCheckEnabled}
onChange={(value) => setParams((prev) => ({ ...prev, balanceCheckEnabled: value }))}
options={[
{ label: "全部状态", value: "" },
{ label: "校验余额模式", value: "true" },
{ label: "无限余额模式", value: "false" },
]}
/>
<Button
type="primary"
icon={<SearchOutlined />}
onClick={() => {
const nextParams = { ...params, current: 1 };
setParams(nextParams);
void loadPlatformPage(nextParams);
}}
>
</Button>
<Button
onClick={() => {
const nextParams = { current: 1, size: 10, tenantName: "", tenantCode: "", balanceCheckEnabled: "" };
setParams(nextParams);
void loadPlatformPage(nextParams);
}}
>
</Button>
</Space>
}
footer={
<AppPagination
current={params.current}
pageSize={params.size}
total={total}
onChange={(page, pageSize) => {
const nextParams = { ...params, current: page, size: pageSize };
setParams(nextParams);
void loadPlatformPage(nextParams);
}}
/>
}
>
<ListTable<TenantMeetingPointsSettingVO>
rowKey="tenantId"
columns={columns}
dataSource={records}
loading={loading}
totalCount={total}
scroll={{x: 1200, y: "100%"}}
pagination={false}
/>
</DataListPanel>
</SectionCard>
) : renderTenantAdminCard()}
</PageContainer>
);
}