import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository, IsNull, Not } from 'typeorm'; import { Room } from '../entities/room.entity'; import { Student } from '../entities/student.entity'; import { Occupancy } from '../entities/occupancy.entity'; import { Bill } from '../entities/bill.entity'; import { RoomExpense } from '../entities/room-expense.entity'; @Injectable() export class DashboardService { constructor( @InjectRepository(Room) private roomRepo: Repository, @InjectRepository(Student) private studentRepo: Repository, @InjectRepository(Occupancy) private occRepo: Repository, @InjectRepository(Bill) private billRepo: Repository, @InjectRepository(RoomExpense) private expRepo: Repository, ) {} async getStats() { const totalRooms = await this.roomRepo.count({ where: { status: Not('archived') } }); const totalStudents = await this.studentRepo.count({ where: { status: 'active' } }); const occupiedBeds = await this.occRepo.count({ where: { checkOutDate: IsNull() } }); const totalCapacity = await this.roomRepo.createQueryBuilder('r') .select('SUM(r.capacity)', 'total') .where('r.status != :archived', { archived: 'archived' }) .getRawOne(); const cap = totalCapacity?.total || 0; const occupancyRate = cap > 0 ? ((occupiedBeds / cap) * 100).toFixed(1) : 0; const billStats = await this.billRepo.createQueryBuilder('b') .select('b.status', 'status') .addSelect('COUNT(*)', 'count') .addSelect('SUM(b.totalAmount)', 'total') .groupBy('b.status') .getRawMany(); return { totalRooms, totalStudents, occupiedBeds, totalCapacity: cap, occupancyRate, billStats }; } // 甘特图数据:每个宿舍的入住时间线 async getGanttData(query?: { periodStart?: string; periodEnd?: string; building?: string }) { const qb = this.occRepo.createQueryBuilder('o') .leftJoinAndSelect('o.student', 'student') .leftJoinAndSelect('o.room', 'room') .where('room.status != :archived', { archived: 'archived' }) .orderBy('room.roomNumber', 'ASC') .addOrderBy('o.checkInDate', 'ASC'); if (query?.building) { qb.andWhere('room.building = :building', { building: query.building }); } if (query?.periodStart) { qb.andWhere('(o.checkOutDate IS NULL OR o.checkOutDate >= :ps)', { ps: query.periodStart }); } if (query?.periodEnd) { qb.andWhere('o.checkInDate <= :pe', { pe: query.periodEnd }); } const records = await qb.getMany(); // 按宿舍分组 const roomMap = new Map(); for (const r of records) { const key = r.room?.roomNumber || String(r.roomId); if (!roomMap.has(key)) roomMap.set(key, []); roomMap.get(key)!.push({ studentName: r.student?.name || '未知', studentId: r.studentId, checkInDate: r.checkInDate, checkOutDate: r.checkOutDate, billingStartDate: r.billingStartDate, billingEndDate: r.billingEndDate, }); } return Array.from(roomMap.entries()).map(([roomNumber, occupancies]) => ({ roomNumber, occupancies, })); } // 费用统计 async getExpenseStats(periodStart?: string, periodEnd?: string) { const qb = this.expRepo.createQueryBuilder('e') .select('e.expenseType', 'type') .addSelect('SUM(e.amount)', 'total') .groupBy('e.expenseType'); if (periodStart) qb.andWhere('e.periodStart >= :ps', { ps: periodStart }); if (periodEnd) qb.andWhere('e.periodEnd <= :pe', { pe: periodEnd }); return qb.getRawMany(); } // 各宿舍费用排行 async getRoomExpenseRanking(periodStart?: string, periodEnd?: string) { const qb = this.expRepo.createQueryBuilder('e') .leftJoin('e.room', 'room') .select('room.roomNumber', 'roomNumber') .addSelect('SUM(e.amount)', 'total') .where('room.status != :archived', { archived: 'archived' }) .groupBy('e.roomId') .orderBy('total', 'DESC') .limit(20); if (periodStart) qb.andWhere('e.periodStart >= :ps', { ps: periodStart }); if (periodEnd) qb.andWhere('e.periodEnd <= :pe', { pe: periodEnd }); return qb.getRawMany(); } }