| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241 |
- 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<string, number>;
- storeComparison: Array<{ storeName: string; groupCount: number }>;
- districtComparison: Array<{ district: string; groupCount: number }>;
- topGroups: GroupChatDto[];
- recentRiskEvents: [];
- stores: StoreDto[];
- /** 上海区级列表(二级) */
- districts: string[];
- /** 按区分组的群列表(三级) */
- groupsByDistrict: Record<string, DashboardScopeGroupOption[]>;
- scope: ResolvedDataScope;
- viewer: Pick<UserDto, 'id' | 'name' | 'role' | 'storeId' | 'storeName' | 'regionCode'>;
- }
- 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<string, number>();
- 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<typeof buildCommunityDistrictIndex>,
- ) {
- const map = new Map<string, number>();
- 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<typeof buildCommunityDistrictIndex>,
- ): Record<string, DashboardScopeGroupOption[]> {
- const map = new Map<string, DashboardScopeGroupOption[]>();
- 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<string, string> = {
- 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<string, number> = {};
- 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<DashboardOverviewDto> {
- 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 '数据范围';
- }
|