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

405 lines
14 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 {
CheckCircleOutlined,
ClockCircleOutlined,
DatabaseOutlined,
ReloadOutlined,
SearchOutlined,
SafetyCertificateOutlined,
} 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, 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;
}
const isBalanceCheckEnabled = currentTenantSetting.balanceCheckEnabled;
return (
<SectionCard
title="当前租户"
description="租户管理员可查看并切换当前租户的积分余额校验模式。"
layout="auto"
className="tenant-meeting-points__tenant-section"
contentClassName="tenant-meeting-points__tenant-content"
>
<div className="tenant-meeting-points__tenant-card">
<div className="tenant-meeting-points__tenant-heading">
<div className="tenant-meeting-points__tenant-identity">
<div className="tenant-meeting-points__tenant-icon">
<SafetyCertificateOutlined />
</div>
<Space direction="vertical" size={4}>
<Text strong className="tenant-meeting-points__tenant-name">
{currentTenantSetting.tenantName || "当前租户"}
</Text>
<Text type="secondary">{currentTenantSetting.tenantCode || "-"}</Text>
</Space>
</div>
<Space className="tenant-meeting-points__tenant-actions" wrap>
{renderStatusTag(isBalanceCheckEnabled)}
{canUpdateBalanceCheck ? (
<Button
type="primary"
loading={switchingTenantId === currentTenantSetting.tenantId}
onClick={() => confirmSwitch(currentTenantSetting, !isBalanceCheckEnabled)}
>
{isBalanceCheckEnabled ? "切换为无限余额" : "开启余额校验"}
</Button>
) : null}
<Button icon={<ReloadOutlined />} onClick={() => void handleRefresh()}>
</Button>
</Space>
</div>
<div
className={
isBalanceCheckEnabled
? "tenant-meeting-points__mode-panel tenant-meeting-points__mode-panel--enabled"
: "tenant-meeting-points__mode-panel tenant-meeting-points__mode-panel--unlimited"
}
>
<div className="tenant-meeting-points__mode-icon">
<CheckCircleOutlined />
</div>
<div className="tenant-meeting-points__mode-copy">
<Text strong>{isBalanceCheckEnabled ? "当前为校验余额模式" : "当前为无限余额模式"}</Text>
<Text type="secondary">
{isBalanceCheckEnabled
? "后续会议提交将按当前账面余额执行拦截与扣减。"
: "后续会议只记录消耗与流水,不扣减账面余额,积分分配也会被禁用。"}
</Text>
</div>
</div>
<div className="tenant-meeting-points__stats">
<div className="tenant-meeting-points__stat-card tenant-meeting-points__stat-card--primary">
<Statistic
title="当前可用额度"
value={isBalanceCheckEnabled ? currentTenantSetting.publicBalance ?? 0 : "无限"}
/>
</div>
<div className="tenant-meeting-points__stat-card">
<Statistic
title="公共账户余额"
value={currentTenantSetting.publicBalance ?? 0}
prefix={<DatabaseOutlined />}
/>
</div>
<div className="tenant-meeting-points__stat-card">
<Statistic title="公共账户累计消耗" value={currentTenantSetting.publicTotalPointsUsed ?? 0} />
</div>
</div>
<div className="tenant-meeting-points__meta">
<div className="tenant-meeting-points__meta-item">
<ClockCircleOutlined />
<span>{formatDateTime(currentTenantSetting.lastSwitchAt)}</span>
</div>
<div className="tenant-meeting-points__meta-item">
<SafetyCertificateOutlined />
<span>{currentTenantSetting.lastSwitchByName || "-"}</span>
</div>
</div>
</div>
</SectionCard>
);
};
return (
<PageContainer
title={null}
className="tenant-meeting-points"
>
{isPlatformAdmin ? (
<SectionCard
title="租户积分校验"
description="按租户控制会议积分是否校验余额,关闭后按无限余额模式记录消耗。"
>
<DataListPanel
className="tenant-meeting-points__list-panel"
toolbarClassName="tenant-meeting-points__toolbar"
leftActions={
<Button icon={<ReloadOutlined />} onClick={() => void handleRefresh()}>
</Button>
}
rightActions={
<Space wrap className="tenant-meeting-points__filters">
<Input
className="tenant-meeting-points__filter tenant-meeting-points__filter--wide"
placeholder="按租户名称搜索"
value={params.tenantName}
onChange={(event) => setParams((prev) => ({ ...prev, tenantName: event.target.value }))}
prefix={<SearchOutlined className="text-gray-400" />}
allowClear
/>
<Input
className="tenant-meeting-points__filter"
placeholder="按租户编码搜索"
value={params.tenantCode}
onChange={(event) => setParams((prev) => ({ ...prev, tenantCode: event.target.value }))}
allowClear
/>
<Select
className="tenant-meeting-points__filter"
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: "max(100%, 1280px)", y: "100%"}}
pagination={false}
/>
</DataListPanel>
</SectionCard>
) : renderTenantAdminCard()}
</PageContainer>
);
}