feat: add tenant dashboard analytics

This commit is contained in:
Codex
2026-06-29 03:52:09 +08:00
parent 21c0634020
commit 0bca1f00a9
8 changed files with 908 additions and 8 deletions

View File

@@ -2599,6 +2599,42 @@ async function testTenantAdminOps() {
});
assert.equal(overview.item?.id, MAIN_TENANT_ID, 'tenant admin overview should belong to main tenant');
const dashboardDenied = await request('/api/tenant-admin/dashboard', {
expectStatus: 403,
});
assert.equal(dashboardDenied.code, 'TENANT_ADMIN_REQUIRED', 'student should not access tenant dashboard');
const dashboard = await request('/api/tenant-admin/dashboard', {
userId: TENANT_ADMIN_USER_ID,
query: { timeRange: '7d', regionId: ids.region },
});
assert.equal(dashboard.item?.scope?.tenantId, MAIN_TENANT_ID, 'tenant dashboard should be tenant scoped');
assert.equal(dashboard.item?.scope?.regionId, ids.region, 'tenant dashboard should support region filter');
assert.equal(dashboard.item?.cards?.content?.questions >= 3, true, 'tenant dashboard should count published questions');
assert.equal(dashboard.item?.cards?.learning?.answers >= 1, true, 'tenant dashboard should aggregate answers');
assert.equal(dashboard.item?.trends?.length, 7, 'tenant dashboard should return complete 7-day trend buckets');
assert.equal(dashboard.item?.activeHours?.length, 24, 'tenant dashboard should return 24 hourly activity buckets');
assert.ok(
dashboard.item?.questionDistribution?.some(item => item.type === 'choice'),
'tenant dashboard should include question type distribution',
);
assert.equal(typeof dashboard.item?.paymentStats?.revenueCentsInRange, 'number', 'tenant dashboard should expose payment stats');
assert.ok(Array.isArray(dashboard.item?.recentActivities), 'tenant dashboard should expose recent activities');
const invalidDashboardRange = await request('/api/tenant-admin/dashboard', {
userId: TENANT_ADMIN_USER_ID,
query: { timeRange: '365d' },
expectStatus: 400,
});
assert.equal(invalidDashboardRange.code, 'INVALID_DASHBOARD_RANGE', 'tenant dashboard should reject unsupported time ranges');
const partnerDashboardDenied = await request('/api/tenant-admin/dashboard', {
tenantId: PARTNER_TENANT_ID,
userId: TENANT_ADMIN_USER_ID,
expectStatus: 403,
});
assert.equal(partnerDashboardDenied.code, 'TENANT_ADMIN_REQUIRED', 'tenant dashboard must be tenant isolated');
const branding = await request('/api/tenant-admin/branding', {
userId: TENANT_ADMIN_USER_ID,
method: 'PUT',
@@ -2928,8 +2964,10 @@ async function testTenantMemberPermissionsAndAudit() {
userId: TENANT_ADMIN_USER_ID,
});
assert.ok(permissionMatrix.permissions?.some(item => item.key === 'marketing:write'), 'permission matrix should expose marketing permission');
assert.ok(permissionMatrix.permissions?.some(item => item.key === 'dashboard:read'), 'permission matrix should expose dashboard read permission');
assert.ok(permissionMatrix.permissions?.some(item => item.key === 'roles:write'), 'permission matrix should expose role template permission');
assert.ok(permissionMatrix.menuGroups?.some(item => item.key === 'sales'), 'permission matrix should expose menu groups');
assert.ok(permissionMatrix.roleDefaults?.tenant_operator?.includes('dashboard:read'), 'tenant operator defaults should include dashboard read');
assert.ok(permissionMatrix.roleDefaults?.tenant_operator?.includes('marketing:*'), 'permission matrix should include role defaults');
const roleTemplate = await request('/api/tenant-admin/role-templates', {
@@ -3018,6 +3056,12 @@ async function testTenantMemberPermissionsAndAudit() {
assert.equal(operatorPermissionMatrix.current?.roleTemplateCode, 'ops-marketing', 'current permission matrix should include role template');
assert.equal(operatorPermissionMatrix.current?.menuPermissions?.marketing, true, 'current permission matrix should expose menu permissions');
const operatorDashboard = await request('/api/tenant-admin/dashboard', {
userId: TENANT_OPERATOR_USER_ID,
query: { timeRange: '7d' },
});
assert.equal(operatorDashboard.item?.scope?.tenantId, MAIN_TENANT_ID, 'tenant operator should read dashboard through default permission');
const operatorBanner = await request('/api/tenant-admin/banners', {
userId: TENANT_OPERATOR_USER_ID,
method: 'PUT',