import type { UserDto } from '../../auth/services/auth.service.js'; import { filterGroupsByDataScope, resolveDataScope, type DataScopeRequest, type ResolvedDataScope, } from '../../../../shared/auth/data-scope.service.js'; import { buildCommunityDistrictIndex, parseShanghaiDistrict, } from '../../../../shared/auth/dashboard-scope.util.js'; import { listGroupChats, type GroupChatDto } from '../../qiwe/services/groups.service.js'; import { listCommunities } from '../../qiwe/services/community.service.js'; import { listStores, type StoreDto } from '../../qiwe/services/organization.service.js'; export interface DashboardStatsDto { totalGroups: number; totalMembers: number; activeGroupRate: number; avgHealthScore: number; complianceRate: number; riskEventCount: number; pendingWorkOrders: number; intentLeadCount: number; conversionRate: number; } export interface DashboardScopeGroupOption { roomId: string; roomName: string; district: string; } export interface DashboardOverviewDto { stats: DashboardStatsDto; activityDistribution: { high: number; medium: number; low: number; inactive: number }; lifecycleDistribution: Array<{ phase: string; label: string; count: number }>; healthGradeDistribution: Record; storeComparison: Array<{ storeName: string; groupCount: number }>; districtComparison: Array<{ district: string; groupCount: number }>; topGroups: GroupChatDto[]; recentRiskEvents: []; stores: StoreDto[]; /** 上海区级列表(二级) */ districts: string[]; /** 按区分组的群列表(三级) */ groupsByDistrict: Record; scope: ResolvedDataScope; viewer: Pick; } function computeStats(groups: GroupChatDto[]): DashboardStatsDto { if (groups.length === 0) { return { totalGroups: 0, totalMembers: 0, activeGroupRate: 0, avgHealthScore: 0, complianceRate: 0, riskEventCount: 0, pendingWorkOrders: 0, intentLeadCount: 0, conversionRate: 0, }; } const totalMembers = groups.reduce((sum, g) => sum + g.memberCount, 0); const activeCount = groups.filter((g) => g.activityLevel === 'high' || g.activityLevel === 'medium').length; const avgHealth = groups.reduce((sum, g) => sum + g.healthScore, 0) / groups.length; const documented = groups.filter((g) => g.hasDocument).length; return { totalGroups: groups.length, totalMembers, activeGroupRate: Math.round((activeCount / groups.length) * 100), avgHealthScore: Math.round(avgHealth), complianceRate: Math.round((documented / groups.length) * 100), riskEventCount: 0, pendingWorkOrders: 0, intentLeadCount: 0, conversionRate: 0, }; } function buildActivityDistribution(groups: GroupChatDto[]) { return { high: groups.filter((g) => g.activityLevel === 'high').length, medium: groups.filter((g) => g.activityLevel === 'medium').length, low: groups.filter((g) => g.activityLevel === 'low').length, inactive: groups.filter((g) => g.activityLevel === 'inactive').length, }; } function buildStoreComparison(groups: GroupChatDto[]) { const storeMap = new Map(); for (const group of groups) { const name = group.storeName || '未绑定门店'; storeMap.set(name, (storeMap.get(name) || 0) + 1); } return Array.from(storeMap.entries()).map(([storeName, groupCount]) => ({ storeName, groupCount, })); } function buildDistrictComparison( groups: GroupChatDto[], communityIndex: ReturnType, ) { const map = new Map(); for (const group of groups) { let district = '未分区'; if (group.communityId) { district = communityIndex.byCommunityId.get(group.communityId) || '未分区'; } else if (group.communityName) { district = parseShanghaiDistrict(group.communityName) || '未分区'; } map.set(district, (map.get(district) || 0) + 1); } return Array.from(map.entries()) .map(([district, groupCount]) => ({ district, groupCount })) .sort((a, b) => b.groupCount - a.groupCount); } function buildGroupsByDistrict( groups: GroupChatDto[], communityIndex: ReturnType, ): Record { const map = new Map(); for (const g of groups) { let district = '未分区'; if (g.communityId) { district = communityIndex.byCommunityId.get(g.communityId) || '未分区'; } const list = map.get(district) || []; list.push({ roomId: g.roomId, roomName: g.roomName || g.roomId, district }); map.set(district, list); } for (const [key, list] of map) { list.sort((a, b) => a.roomName.localeCompare(b.roomName, 'zh-CN')); map.set(key, list); } return Object.fromEntries(map); } function buildLifecycleDistribution(groups: GroupChatDto[]) { const labels: Record = { pre_handover: '交房前', near_handover: '临交房', post_handover: '已交房', legacy: '老小区', }; const phases = ['pre_handover', 'near_handover', 'post_handover', 'legacy']; return phases.map((phase) => ({ phase, label: labels[phase] || phase, count: groups.filter((g) => g.lifecyclePhase === phase).length, })); } function buildHealthGradeDistribution(groups: GroupChatDto[]) { const grades = ['S', 'A', 'B', 'C']; const result: Record = {}; for (const grade of grades) { result[grade] = groups.filter((g) => g.healthGrade === grade).length; } return result; } export async function getDashboardOverview( user: UserDto, scopeRequest: DataScopeRequest = {}, ): Promise { const [allGroups, stores, communities, scope] = await Promise.all([ listGroupChats(), listStores(), listCommunities(), resolveDataScope(user, scopeRequest), ]); const communityIndex = buildCommunityDistrictIndex(communities); const cityScoped = filterGroupsByDataScope(allGroups, { ...scope, level: scope.level === 'district' || scope.level === 'group' ? 'city' : scope.level, district: undefined, roomId: undefined, }, communityIndex); const groups = filterGroupsByDataScope(allGroups, scope, communityIndex); const topGroups = [...groups] .sort((a, b) => b.messageCountToday - a.messageCountToday) .slice(0, 10); const groupsByDistrict = buildGroupsByDistrict(cityScoped, communityIndex); const districts = [ ...communityIndex.districts, ...(groupsByDistrict['未分区']?.length ? ['未分区'] : []), ].filter((d, i, arr) => arr.indexOf(d) === i); return { stats: computeStats(groups), activityDistribution: buildActivityDistribution(groups), lifecycleDistribution: buildLifecycleDistribution(groups), healthGradeDistribution: buildHealthGradeDistribution(groups), storeComparison: buildStoreComparison(scope.level === 'global' ? allGroups : groups), districtComparison: buildDistrictComparison( scope.level === 'city' ? cityScoped : groups, communityIndex, ), topGroups, recentRiskEvents: [], stores, districts, groupsByDistrict, scope: { ...scope, label: scope.label || resolveScopeLabel(scope), }, viewer: { id: user.id, name: user.name, role: user.role, storeId: user.storeId, storeName: user.storeName, regionCode: user.regionCode, }, }; } function resolveScopeLabel(scope: ResolvedDataScope): string { if (scope.label) return scope.label; if (scope.level === 'city') return '全上海'; if (scope.level === 'district' && scope.district) return scope.district; if (scope.level === 'group') return '单个客户群'; if (scope.level === 'global') return '全部门店汇总'; if (scope.storeName) return scope.storeName; return '数据范围'; }