Files
gongxue-base/apps/server/src/dashboard/dashboard.service.ts
wangziqi 46a817503e feat(task1): restructure directories for turborepo monorepo
- Move backend/ to apps/server/ via git mv
- Move frontend/ to apps/admin/ via git mv
- Create packages/typescript-config/ with base, nestjs, and react-vite presets
2026-07-02 15:05:12 +08:00

109 lines
4.2 KiB
TypeScript

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<Room>,
@InjectRepository(Student) private studentRepo: Repository<Student>,
@InjectRepository(Occupancy) private occRepo: Repository<Occupancy>,
@InjectRepository(Bill) private billRepo: Repository<Bill>,
@InjectRepository(RoomExpense) private expRepo: Repository<RoomExpense>,
) {}
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<string, any[]>();
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();
}
}