forked from wangziqi/gongxue-base
feat: bind tenant members to role templates
This commit is contained in:
@@ -2,14 +2,18 @@ import { useEffect, useMemo, useState } from 'react';
|
||||
import Taro from '@tarojs/taro';
|
||||
import { Button, Input, Text, Textarea, View } from '@tarojs/components';
|
||||
import {
|
||||
disableTenantMember,
|
||||
disableRoleTemplate,
|
||||
loadAuthProviders,
|
||||
loadTenantMembers,
|
||||
loadPaymentAccounts,
|
||||
loadRoleTemplates,
|
||||
loadTenantDomains,
|
||||
loadTenantOverview,
|
||||
loadTenantPermissions,
|
||||
upsertTenantMember,
|
||||
upsertRoleTemplate,
|
||||
type TenantMemberItem,
|
||||
type TenantOverview,
|
||||
type TenantPermissionCatalogItem,
|
||||
type TenantPermissionsPayload,
|
||||
@@ -35,6 +39,19 @@ interface RoleTemplateForm {
|
||||
ownLeadsOnly: boolean;
|
||||
}
|
||||
|
||||
interface MemberForm {
|
||||
membershipId?: string;
|
||||
userId: string;
|
||||
username: string;
|
||||
name: string;
|
||||
phone: string;
|
||||
email: string;
|
||||
role: string;
|
||||
status: string;
|
||||
roleTemplateId: string;
|
||||
permissions: Record<string, boolean>;
|
||||
}
|
||||
|
||||
const BASE_ROLE_OPTIONS = [
|
||||
{ key: 'tenant_operator', label: '运营' },
|
||||
{ key: 'teacher', label: '教师' },
|
||||
@@ -44,6 +61,12 @@ const BASE_ROLE_OPTIONS = [
|
||||
{ key: 'tenant_admin', label: '管理员' },
|
||||
];
|
||||
|
||||
const MEMBER_STATUS_OPTIONS = [
|
||||
{ key: 'active', label: '启用' },
|
||||
{ key: 'invited', label: '邀请中' },
|
||||
{ key: 'disabled', label: '停用' },
|
||||
];
|
||||
|
||||
const STATUS_OPTIONS = [
|
||||
{ key: 'active', label: '启用' },
|
||||
{ key: 'disabled', label: '停用' },
|
||||
@@ -103,6 +126,20 @@ function emptyRoleTemplateForm(): RoleTemplateForm {
|
||||
};
|
||||
}
|
||||
|
||||
function emptyMemberForm(): MemberForm {
|
||||
return {
|
||||
userId: '',
|
||||
username: '',
|
||||
name: '',
|
||||
phone: '',
|
||||
email: '',
|
||||
role: 'tenant_operator',
|
||||
status: 'active',
|
||||
roleTemplateId: '',
|
||||
permissions: {},
|
||||
};
|
||||
}
|
||||
|
||||
function roleTemplateFormFrom(item?: TenantRoleTemplateItem | null): RoleTemplateForm {
|
||||
if (!item) return emptyRoleTemplateForm();
|
||||
const dataScope = item.dataScope && typeof item.dataScope === 'object' ? item.dataScope : {};
|
||||
@@ -123,6 +160,22 @@ function roleTemplateFormFrom(item?: TenantRoleTemplateItem | null): RoleTemplat
|
||||
};
|
||||
}
|
||||
|
||||
function memberFormFrom(item?: TenantMemberItem | null): MemberForm {
|
||||
if (!item) return emptyMemberForm();
|
||||
return {
|
||||
membershipId: item.id,
|
||||
userId: item.userId || '',
|
||||
username: item.username || '',
|
||||
name: item.name || '',
|
||||
phone: item.phone || '',
|
||||
email: item.email || '',
|
||||
role: item.role || 'tenant_operator',
|
||||
status: item.status || 'active',
|
||||
roleTemplateId: item.roleTemplateId || '',
|
||||
permissions: boolRecord(item.permissions),
|
||||
};
|
||||
}
|
||||
|
||||
function catalogLabel(item: TenantPermissionCatalogItem) {
|
||||
return item.label ? `${item.label}` : item.key;
|
||||
}
|
||||
@@ -133,9 +186,13 @@ export default function TenantSettingsPage() {
|
||||
const [payments, setPayments] = useState<Record<string, unknown>[]>([]);
|
||||
const [authProviders, setAuthProviders] = useState<Record<string, unknown>[]>([]);
|
||||
const [roles, setRoles] = useState<TenantRoleTemplateItem[]>([]);
|
||||
const [members, setMembers] = useState<TenantMemberItem[]>([]);
|
||||
const [permissionsPayload, setPermissionsPayload] = useState<TenantPermissionsPayload>({});
|
||||
const [selectedRoleId, setSelectedRoleId] = useState('');
|
||||
const [roleForm, setRoleForm] = useState<RoleTemplateForm>(() => emptyRoleTemplateForm());
|
||||
const [memberKeyword, setMemberKeyword] = useState('');
|
||||
const [selectedMemberId, setSelectedMemberId] = useState('');
|
||||
const [memberForm, setMemberForm] = useState<MemberForm>(() => emptyMemberForm());
|
||||
const [busy, setBusy] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
|
||||
@@ -145,6 +202,7 @@ export default function TenantSettingsPage() {
|
||||
);
|
||||
const fieldCatalog = permissionsPayload.fieldGroups || permissionsPayload.fieldCatalog || [];
|
||||
const editingSystemRole = Boolean(selectedRole?.isSystem);
|
||||
const activeRoleTemplates = roles.filter(item => item.status === 'active');
|
||||
|
||||
async function reloadSettings(preferredRoleId = selectedRoleId) {
|
||||
setError('');
|
||||
@@ -172,8 +230,23 @@ export default function TenantSettingsPage() {
|
||||
}
|
||||
}
|
||||
|
||||
async function reloadMembers(preferredMemberId = selectedMemberId, keyword = memberKeyword) {
|
||||
setError('');
|
||||
try {
|
||||
const payload = await loadTenantMembers({ keyword, limit: 80 });
|
||||
const nextMembers = payload.items || [];
|
||||
const nextSelected = nextMembers.find(item => item.id === preferredMemberId) || nextMembers[0] || null;
|
||||
setMembers(nextMembers);
|
||||
setSelectedMemberId(nextSelected?.id || '');
|
||||
setMemberForm(nextSelected ? memberFormFrom(nextSelected) : emptyMemberForm());
|
||||
} catch (nextError) {
|
||||
setError(nextError instanceof Error ? nextError.message : '成员加载失败');
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
void reloadSettings('');
|
||||
void reloadMembers('');
|
||||
}, []);
|
||||
|
||||
function startNewRole() {
|
||||
@@ -186,6 +259,16 @@ export default function TenantSettingsPage() {
|
||||
setRoleForm(roleTemplateFormFrom(item));
|
||||
}
|
||||
|
||||
function startNewMember() {
|
||||
setSelectedMemberId('');
|
||||
setMemberForm(emptyMemberForm());
|
||||
}
|
||||
|
||||
function selectMember(item: TenantMemberItem) {
|
||||
setSelectedMemberId(item.id);
|
||||
setMemberForm(memberFormFrom(item));
|
||||
}
|
||||
|
||||
function updateBooleanMap(field: BooleanMapField, key: string) {
|
||||
setRoleForm(prev => ({
|
||||
...prev,
|
||||
@@ -196,6 +279,16 @@ export default function TenantSettingsPage() {
|
||||
}));
|
||||
}
|
||||
|
||||
function updateMemberPermission(key: string) {
|
||||
setMemberForm(prev => ({
|
||||
...prev,
|
||||
permissions: {
|
||||
...prev.permissions,
|
||||
[key]: !prev.permissions[key],
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
async function submitRoleTemplate() {
|
||||
if (!roleForm.name.trim() || !roleForm.code.trim()) {
|
||||
Taro.showToast({ title: '名称和编码必填', icon: 'none' });
|
||||
@@ -267,6 +360,60 @@ export default function TenantSettingsPage() {
|
||||
}
|
||||
}
|
||||
|
||||
async function submitMember() {
|
||||
const hasIdentity = memberForm.userId.trim() || memberForm.phone.trim() || memberForm.email.trim() || memberForm.username.trim() || memberForm.name.trim();
|
||||
if (!hasIdentity) {
|
||||
Taro.showToast({ title: '请填写成员身份', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
setBusy('save-member');
|
||||
setError('');
|
||||
try {
|
||||
const selectedTemplate = activeRoleTemplates.find(item => item.id === memberForm.roleTemplateId);
|
||||
const result = await upsertTenantMember({
|
||||
membershipId: memberForm.membershipId,
|
||||
userId: memberForm.userId.trim() || undefined,
|
||||
username: memberForm.username.trim() || null,
|
||||
name: memberForm.name.trim() || null,
|
||||
phone: memberForm.phone.trim() || null,
|
||||
email: memberForm.email.trim() || null,
|
||||
primaryRole: selectedTemplate?.baseRole || memberForm.role,
|
||||
role: selectedTemplate?.baseRole || memberForm.role,
|
||||
status: memberForm.status,
|
||||
roleTemplateId: memberForm.roleTemplateId || null,
|
||||
permissions: memberForm.permissions,
|
||||
});
|
||||
const nextId = result.item?.id || memberForm.membershipId || '';
|
||||
Taro.showToast({ title: '成员已保存', icon: 'success' });
|
||||
await reloadMembers(nextId);
|
||||
} catch (nextError) {
|
||||
setError(nextError instanceof Error ? nextError.message : '成员保存失败');
|
||||
} finally {
|
||||
setBusy('');
|
||||
}
|
||||
}
|
||||
|
||||
async function disableMember(item: TenantMemberItem) {
|
||||
const confirmed = await Taro.showModal({
|
||||
title: '停用成员',
|
||||
content: `确认停用 ${item.name || item.username || item.phone || item.email || item.userId}?`,
|
||||
confirmText: '停用',
|
||||
cancelText: '取消',
|
||||
});
|
||||
if (!confirmed.confirm) return;
|
||||
setBusy(`disable-member:${item.id}`);
|
||||
setError('');
|
||||
try {
|
||||
await disableTenantMember(item.id);
|
||||
Taro.showToast({ title: '成员已停用', icon: 'success' });
|
||||
await reloadMembers(item.id);
|
||||
} catch (nextError) {
|
||||
setError(nextError instanceof Error ? nextError.message : '成员停用失败');
|
||||
} finally {
|
||||
setBusy('');
|
||||
}
|
||||
}
|
||||
|
||||
function renderToggleGroup(items: TenantPermissionCatalogItem[], field: BooleanMapField) {
|
||||
if (!items.length) return <View className='admin-empty'>暂无可配置项。</View>;
|
||||
return (
|
||||
@@ -284,6 +431,23 @@ export default function TenantSettingsPage() {
|
||||
);
|
||||
}
|
||||
|
||||
function renderMemberPermissionGroup(items: TenantPermissionCatalogItem[]) {
|
||||
if (!items.length) return <View className='admin-empty'>暂无可配置项。</View>;
|
||||
return (
|
||||
<View className='admin-actions compact'>
|
||||
{items.map(item => (
|
||||
<Button
|
||||
key={item.key}
|
||||
className={`admin-mini-button ${memberForm.permissions[item.key] ? 'primary' : ''}`}
|
||||
onClick={() => updateMemberPermission(item.key)}
|
||||
>
|
||||
{catalogLabel(item)}
|
||||
</Button>
|
||||
))}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View className='admin-page'>
|
||||
<View className='admin-shell'>
|
||||
@@ -473,6 +637,139 @@ export default function TenantSettingsPage() {
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View className='admin-section'>
|
||||
<View className='admin-actions compact'>
|
||||
<Text className='admin-section-title'>成员与模板绑定</Text>
|
||||
<Button className='admin-button primary' onClick={startNewMember}>新建成员</Button>
|
||||
</View>
|
||||
<View className='admin-form-grid'>
|
||||
<Input
|
||||
className='admin-input'
|
||||
placeholder='搜索姓名、手机号、邮箱、用户名'
|
||||
value={memberKeyword}
|
||||
onInput={event => setMemberKeyword(String(event.detail.value || ''))}
|
||||
/>
|
||||
<Button className='admin-button primary' onClick={() => reloadMembers('', memberKeyword)}>搜索成员</Button>
|
||||
</View>
|
||||
<View className='admin-list'>
|
||||
{members.map(item => (
|
||||
<View className='admin-row' key={item.id}>
|
||||
<Text className='admin-row-main'>{item.name || item.username || item.phone || item.email || item.userId}</Text>
|
||||
<Text className='admin-row-meta'>{item.role} · {item.status || 'active'} · {item.roleTemplateName || item.roleTemplateCode || '未绑定模板'}</Text>
|
||||
<Text className='admin-row-meta'>{item.phone || '-'} · {item.email || '-'}</Text>
|
||||
<View className='admin-row-actions'>
|
||||
<Button className='admin-mini-button primary' onClick={() => selectMember(item)}>编辑</Button>
|
||||
{item.status !== 'disabled' ? (
|
||||
<Button className='admin-mini-button' loading={busy === `disable-member:${item.id}`} onClick={() => disableMember(item)}>停用</Button>
|
||||
) : null}
|
||||
</View>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
{!members.length ? <View className='admin-empty'>暂无成员或搜索无结果。</View> : null}
|
||||
</View>
|
||||
|
||||
<View className='admin-section'>
|
||||
<Text className='admin-section-title'>{selectedMemberId ? '编辑成员权限' : '新建成员权限'}</Text>
|
||||
<View className='admin-form-grid'>
|
||||
<Input
|
||||
className='admin-input'
|
||||
placeholder='已有用户 ID,可留空用手机号/邮箱创建或匹配'
|
||||
value={memberForm.userId}
|
||||
disabled={Boolean(memberForm.membershipId)}
|
||||
onInput={event => setMemberForm(prev => ({ ...prev, userId: String(event.detail.value || '') }))}
|
||||
/>
|
||||
<Input
|
||||
className='admin-input'
|
||||
placeholder='姓名'
|
||||
value={memberForm.name}
|
||||
onInput={event => setMemberForm(prev => ({ ...prev, name: String(event.detail.value || '') }))}
|
||||
/>
|
||||
<Input
|
||||
className='admin-input'
|
||||
placeholder='用户名'
|
||||
value={memberForm.username}
|
||||
onInput={event => setMemberForm(prev => ({ ...prev, username: String(event.detail.value || '') }))}
|
||||
/>
|
||||
<Input
|
||||
className='admin-input'
|
||||
placeholder='手机号'
|
||||
type='number'
|
||||
value={memberForm.phone}
|
||||
onInput={event => setMemberForm(prev => ({ ...prev, phone: String(event.detail.value || '') }))}
|
||||
/>
|
||||
<Input
|
||||
className='admin-input'
|
||||
placeholder='邮箱'
|
||||
value={memberForm.email}
|
||||
onInput={event => setMemberForm(prev => ({ ...prev, email: String(event.detail.value || '') }))}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View className='admin-section'>
|
||||
<Text className='admin-section-title'>绑定角色模板</Text>
|
||||
<View className='admin-actions compact'>
|
||||
<Button
|
||||
className={`admin-button ${memberForm.roleTemplateId ? '' : 'active'}`}
|
||||
onClick={() => setMemberForm(prev => ({ ...prev, roleTemplateId: '' }))}
|
||||
>
|
||||
不绑定
|
||||
</Button>
|
||||
{activeRoleTemplates.map(item => (
|
||||
<Button
|
||||
key={item.id}
|
||||
className={`admin-button ${memberForm.roleTemplateId === item.id ? 'active' : ''}`}
|
||||
onClick={() => setMemberForm(prev => ({ ...prev, roleTemplateId: item.id, role: item.baseRole || prev.role }))}
|
||||
>
|
||||
{item.name || item.code}
|
||||
</Button>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View className='admin-section'>
|
||||
<Text className='admin-section-title'>成员基础角色</Text>
|
||||
<View className='admin-actions compact'>
|
||||
{BASE_ROLE_OPTIONS.map(item => (
|
||||
<Button
|
||||
key={item.key}
|
||||
className={`admin-button ${memberForm.role === item.key ? 'active' : ''}`}
|
||||
onClick={() => setMemberForm(prev => ({ ...prev, role: item.key, roleTemplateId: '' }))}
|
||||
>
|
||||
{item.label}
|
||||
</Button>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View className='admin-section'>
|
||||
<Text className='admin-section-title'>成员状态</Text>
|
||||
<View className='admin-actions compact'>
|
||||
{MEMBER_STATUS_OPTIONS.map(item => (
|
||||
<Button
|
||||
key={item.key}
|
||||
className={`admin-button ${memberForm.status === item.key ? 'active' : ''}`}
|
||||
onClick={() => setMemberForm(prev => ({ ...prev, status: item.key }))}
|
||||
>
|
||||
{item.label}
|
||||
</Button>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View className='admin-section'>
|
||||
<Text className='admin-section-title'>成员额外权限覆盖</Text>
|
||||
{renderMemberPermissionGroup(permissionsPayload.permissions || [])}
|
||||
</View>
|
||||
|
||||
<View className='admin-section'>
|
||||
<View className='admin-actions compact'>
|
||||
<Button className='admin-button primary' loading={busy === 'save-member'} onClick={submitMember}>保存成员</Button>
|
||||
<Button className='admin-button' onClick={() => reloadMembers(selectedMemberId)}>刷新成员</Button>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{error ? <Text className='admin-error'>{error}</Text> : null}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@@ -2,28 +2,84 @@ import { useEffect, useState } from 'react';
|
||||
import Taro from '@tarojs/taro';
|
||||
import { Button, Text, View } from '@tarojs/components';
|
||||
import { getTenantContext } from '@/services/api';
|
||||
import { loadTenantDashboard, loadTenantOverview, type TenantDashboard, type TenantOverview } from '@/services/tenantAdmin';
|
||||
import {
|
||||
loadTenantDashboard,
|
||||
loadTenantOverview,
|
||||
loadTenantPermissions,
|
||||
type TenantDashboard,
|
||||
type TenantOverview,
|
||||
type TenantPermissionsPayload,
|
||||
} from '@/services/tenantAdmin';
|
||||
import '../admin.css';
|
||||
|
||||
interface AdminModule {
|
||||
key: string;
|
||||
name: string;
|
||||
path: string;
|
||||
meta: string;
|
||||
permission?: string;
|
||||
}
|
||||
|
||||
const MODULES: AdminModule[] = [
|
||||
{ key: 'dashboard', name: '数据看板', path: '/pages/tenant-admin/dashboard/index', meta: '收益、注册、学习、内容', permission: 'dashboard:read' },
|
||||
{ key: 'students', name: '学生运营', path: '/pages/tenant-admin/students/index', meta: '学生、班级、教师范围', permission: 'students:read' },
|
||||
{ key: 'content', name: '题库内容', path: '/pages/tenant-admin/content/index', meta: '入口、导入、公共题库', permission: 'content:*' },
|
||||
{ key: 'marketing', name: '营销中心', path: '/pages/tenant-admin/marketing/index', meta: '优惠券、激活码、分佣', permission: 'marketing:read' },
|
||||
{ key: 'settings', name: '租户设置', path: '/pages/tenant-admin/settings/index', meta: '品牌、域名、支付、登录、角色', permission: 'tenant:overview:read' },
|
||||
];
|
||||
|
||||
function boolRecord(value: unknown) {
|
||||
return value && typeof value === 'object' && !Array.isArray(value) ? value as Record<string, unknown> : {};
|
||||
}
|
||||
|
||||
function explicitPermission(permissions: Record<string, unknown>, permission: string) {
|
||||
const parts = permission.split(':').filter(Boolean);
|
||||
const candidates = [permission];
|
||||
for (let index = parts.length - 1; index >= 1; index -= 1) {
|
||||
candidates.push(`${parts.slice(0, index).join(':')}:*`);
|
||||
}
|
||||
candidates.push('*');
|
||||
for (const key of candidates) {
|
||||
if (typeof permissions[key] === 'boolean') return permissions[key] as boolean;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function canOpenModule(payload: TenantPermissionsPayload, item: AdminModule) {
|
||||
const current = payload.current || {};
|
||||
const menuPermissions = boolRecord(current.menuPermissions);
|
||||
if (typeof menuPermissions[item.key] === 'boolean') return menuPermissions[item.key] as boolean;
|
||||
|
||||
if (!item.permission) return true;
|
||||
const effectivePermissions = boolRecord(current.effectivePermissions);
|
||||
const explicit = explicitPermission(effectivePermissions, item.permission);
|
||||
if (explicit !== null) return explicit;
|
||||
|
||||
const role = String(current.role || '');
|
||||
const defaults = payload.roleDefaults?.[role] || [];
|
||||
return defaults.some(permission => {
|
||||
if (permission === '*') return true;
|
||||
if (permission === item.permission) return true;
|
||||
if (permission.endsWith(':*')) return item.permission?.startsWith(permission.slice(0, -1));
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
export default function TenantWorkbenchPage() {
|
||||
const tenant = getTenantContext();
|
||||
const [dashboard, setDashboard] = useState<TenantDashboard | null>(null);
|
||||
const [overview, setOverview] = useState<TenantOverview | null>(null);
|
||||
const [permissions, setPermissions] = useState<TenantPermissionsPayload>({});
|
||||
|
||||
useEffect(() => {
|
||||
loadTenantDashboard('30d').then(payload => setDashboard(payload.item || null)).catch(() => setDashboard(null));
|
||||
loadTenantOverview().then(payload => setOverview(payload.item || null)).catch(() => setOverview(null));
|
||||
loadTenantPermissions().then(payload => setPermissions(payload)).catch(() => setPermissions({}));
|
||||
}, []);
|
||||
|
||||
const cards = dashboard?.cards || {};
|
||||
const payment = dashboard?.paymentStats || {};
|
||||
const modules = [
|
||||
{ name: '数据看板', path: '/pages/tenant-admin/dashboard/index', meta: '收益、注册、学习、内容' },
|
||||
{ name: '学生运营', path: '/pages/tenant-admin/students/index', meta: '学生、班级、教师范围' },
|
||||
{ name: '题库内容', path: '/pages/tenant-admin/content/index', meta: '入口、导入、公共题库' },
|
||||
{ name: '营销中心', path: '/pages/tenant-admin/marketing/index', meta: '优惠券、激活码、分佣' },
|
||||
{ name: '租户设置', path: '/pages/tenant-admin/settings/index', meta: '品牌、域名、支付、登录' },
|
||||
];
|
||||
const modules = MODULES.filter(item => canOpenModule(permissions, item));
|
||||
|
||||
return (
|
||||
<View className='admin-page'>
|
||||
@@ -63,6 +119,7 @@ export default function TenantWorkbenchPage() {
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
{!modules.length ? <View className='admin-empty'>当前角色暂无可见后台模块。</View> : null}
|
||||
</View>
|
||||
|
||||
<View className='admin-actions'>
|
||||
|
||||
@@ -297,6 +297,45 @@ export interface TenantRoleTemplateInput {
|
||||
sortOrder?: number;
|
||||
}
|
||||
|
||||
export interface TenantMemberItem {
|
||||
id: string;
|
||||
userId: string;
|
||||
username?: string | null;
|
||||
name?: string | null;
|
||||
phone?: string | null;
|
||||
email?: string | null;
|
||||
avatarUrl?: string | null;
|
||||
primaryRole?: string | null;
|
||||
role: string;
|
||||
status?: string;
|
||||
permissions?: Record<string, boolean>;
|
||||
roleTemplateId?: string | null;
|
||||
roleTemplateCode?: string | null;
|
||||
roleTemplateName?: string | null;
|
||||
menuPermissions?: Record<string, boolean>;
|
||||
modulePermissions?: Record<string, boolean>;
|
||||
fieldPermissions?: Record<string, boolean>;
|
||||
dataScope?: Record<string, unknown>;
|
||||
legacyRole?: string | null;
|
||||
lastSeenAt?: string | null;
|
||||
createdAt?: string;
|
||||
updatedAt?: string;
|
||||
}
|
||||
|
||||
export interface TenantMemberInput {
|
||||
membershipId?: string;
|
||||
userId?: string;
|
||||
username?: string | null;
|
||||
name?: string | null;
|
||||
phone?: string | null;
|
||||
email?: string | null;
|
||||
primaryRole?: string | null;
|
||||
role: string;
|
||||
status?: string;
|
||||
permissions?: Record<string, boolean>;
|
||||
roleTemplateId?: string | null;
|
||||
}
|
||||
|
||||
export async function loadTenantDashboard(timeRange = '30d') {
|
||||
return apiRequest<{ item?: TenantDashboard }>('/api/tenant-admin/dashboard', { query: { timeRange } });
|
||||
}
|
||||
@@ -459,6 +498,26 @@ export async function disableRoleTemplate(roleTemplateId: string) {
|
||||
});
|
||||
}
|
||||
|
||||
export async function loadTenantMembers(query: { keyword?: string; role?: string; status?: string; limit?: number } = {}) {
|
||||
return apiRequest<{ items?: TenantMemberItem[] }>('/api/tenant-admin/members', {
|
||||
query: { ...query, limit: query.limit || 50 },
|
||||
});
|
||||
}
|
||||
|
||||
export async function upsertTenantMember(input: TenantMemberInput) {
|
||||
return apiRequest<{ item?: TenantMemberItem }>('/api/tenant-admin/members', {
|
||||
method: 'PUT',
|
||||
body: input,
|
||||
});
|
||||
}
|
||||
|
||||
export async function disableTenantMember(membershipId: string) {
|
||||
return apiRequest<{ item?: TenantMemberItem }>('/api/tenant-admin/members/disable', {
|
||||
method: 'POST',
|
||||
body: { membershipId },
|
||||
});
|
||||
}
|
||||
|
||||
export async function loadCrmQueue(limit = 20) {
|
||||
return apiRequest<{ items?: Record<string, unknown>[] }>('/api/crm/queue', { query: { limit } });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user