From d317cb4fc1e4930df01d37150531a864eb2cc577 Mon Sep 17 00:00:00 2001 From: wangziqi Date: Sun, 5 Jul 2026 19:51:16 +0800 Subject: [PATCH] feat(dashboard): add classroom/attendance/income stats to dashboard API - Add classroomCount, classroomOccupancyRate from Classroom + ClassSchedule - Add todayAttendanceRate, attendanceTrend from AttendanceRecord - Add monthlyIncome, incomeTrend from Bill - Register Classroom, ClassSchedule, AttendanceRecord in dashboard module --- apps/server/src/dashboard/dashboard.module.ts | 5 +- .../server/src/dashboard/dashboard.service.ts | 121 ++++++++++++++++++ 2 files changed, 125 insertions(+), 1 deletion(-) diff --git a/apps/server/src/dashboard/dashboard.module.ts b/apps/server/src/dashboard/dashboard.module.ts index eb6f68b..cd3a283 100644 --- a/apps/server/src/dashboard/dashboard.module.ts +++ b/apps/server/src/dashboard/dashboard.module.ts @@ -5,11 +5,14 @@ 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'; +import { Classroom } from '../entities/classroom.entity'; +import { ClassSchedule } from '../entities/class-schedule.entity'; +import { AttendanceRecord } from '../entities/attendance-record.entity'; import { DashboardService } from './dashboard.service'; import { DashboardController } from './dashboard.controller'; @Module({ - imports: [TypeOrmModule.forFeature([Room, Student, Occupancy, Bill, RoomExpense])], + imports: [TypeOrmModule.forFeature([Room, Student, Occupancy, Bill, RoomExpense, Classroom, ClassSchedule, AttendanceRecord])], controllers: [DashboardController], providers: [DashboardService], }) diff --git a/apps/server/src/dashboard/dashboard.service.ts b/apps/server/src/dashboard/dashboard.service.ts index 6d33dcf..0ff923c 100644 --- a/apps/server/src/dashboard/dashboard.service.ts +++ b/apps/server/src/dashboard/dashboard.service.ts @@ -6,6 +6,9 @@ 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'; +import { Classroom } from '../entities/classroom.entity'; +import { ClassSchedule } from '../entities/class-schedule.entity'; +import { AttendanceRecord } from '../entities/attendance-record.entity'; @Injectable() export class DashboardService { @@ -15,9 +18,16 @@ export class DashboardService { @InjectRepository(Occupancy) private occRepo: Repository, @InjectRepository(Bill) private billRepo: Repository, @InjectRepository(RoomExpense) private expRepo: Repository, + @InjectRepository(Classroom) private classroomRepo: Repository, + @InjectRepository(ClassSchedule) private scheduleRepo: Repository, + @InjectRepository(AttendanceRecord) private attendanceRepo: Repository, ) {} async getStats() { + const today = new Date(); + const todayStr = today.toISOString().slice(0, 10); + const currentMonth = todayStr.slice(0, 7); // YYYY-MM + 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() } }); @@ -37,6 +47,48 @@ export class DashboardService { .groupBy('b.status') .getRawMany(); + // New fields + const classroomCount = await this.classroomRepo.count(); + + const occResult = await this.scheduleRepo + .createQueryBuilder('s') + .select('COUNT(DISTINCT s.classroomId)', 'cnt') + .where('s.status = :active', { active: 'active' }) + .andWhere('s.startDate <= :today', { today: todayStr }) + .andWhere('s.endDate >= :today', { today: todayStr }) + .getRawOne(); + const occupiedClassrooms = parseInt(occResult?.cnt || '0', 10); + const classroomOccupancyRate = classroomCount > 0 + ? ((occupiedClassrooms / classroomCount) * 100).toFixed(1) + : 0; + + const attTodayStats = await this.attendanceRepo + .createQueryBuilder('a') + .select('a.status', 'status') + .addSelect('COUNT(*)', 'count') + .where('a.attendanceDate = :today', { today: todayStr }) + .groupBy('a.status') + .getRawMany(); + const todayTotal = attTodayStats.reduce((sum, r) => sum + parseInt(r.count, 10), 0); + const todayPresent = attTodayStats + .filter((r) => r.status === 'present') + .reduce((sum, r) => sum + parseInt(r.count, 10), 0); + const todayAttendanceRate = todayTotal > 0 + ? ((todayPresent / todayTotal) * 100).toFixed(1) + : 0; + + const incomeResult = await this.billRepo + .createQueryBuilder('b') + .select('SUM(b.totalAmount)', 'total') + .where('b.status = :paid', { paid: 'paid' }) + .andWhere('b.periodStart >= :start', { start: `${currentMonth}-01` }) + .andWhere('b.periodStart < :end', { end: this.nextMonth(currentMonth) }) + .getRawOne(); + const monthlyIncome = parseFloat(incomeResult?.total || '0'); + + const attendanceTrend = await this.getAttendanceTrend(todayStr); + const incomeTrend = await this.getIncomeTrend(currentMonth); + return { totalRooms, totalStudents, @@ -44,9 +96,78 @@ export class DashboardService { totalCapacity: cap, occupancyRate, billStats, + classroomCount, + classroomOccupancyRate, + todayAttendanceRate, + monthlyIncome, + attendanceTrend, + incomeTrend, }; } + private async getAttendanceTrend(todayStr: string) { + const thirtyDaysAgo = new Date(todayStr); + thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 29); + const startStr = thirtyDaysAgo.toISOString().slice(0, 10); + + const rows = await this.attendanceRepo + .createQueryBuilder('a') + .select('a.attendanceDate', 'date') + .addSelect('a.status', 'status') + .addSelect('COUNT(*)', 'count') + .where('a.attendanceDate >= :start', { start: startStr }) + .andWhere('a.attendanceDate <= :today', { today: todayStr }) + .groupBy('a.attendanceDate') + .addGroupBy('a.status') + .orderBy('a.attendanceDate', 'ASC') + .getRawMany(); + + const dayMap = new Map(); + for (const row of rows) { + const d = dayMap.get(row.date) || { total: 0, present: 0 }; + const cnt = parseInt(row.count, 10); + d.total += cnt; + if (row.status === 'present') d.present += cnt; + dayMap.set(row.date, d); + } + + return Array.from(dayMap.entries()).map(([date, d]) => ({ + date, + rate: d.total > 0 ? ((d.present / d.total) * 100).toFixed(1) : 0, + })); + } + + private async getIncomeTrend(currentMonth: string) { + const results: { month: string; amount: number }[] = []; + + for (let i = 5; i >= 0; i--) { + const d = new Date(`${currentMonth}-01`); + d.setMonth(d.getMonth() - i); + const m = d.toISOString().slice(0, 7); + + const row = await this.billRepo + .createQueryBuilder('b') + .select('SUM(b.totalAmount)', 'total') + .where('b.status = :paid', { paid: 'paid' }) + .andWhere('b.periodStart >= :start', { start: `${m}-01` }) + .andWhere('b.periodStart < :end', { end: this.nextMonth(m) }) + .getRawOne(); + + results.push({ + month: m, + amount: parseFloat(row?.total || '0'), + }); + } + + return results; + } + + private nextMonth(ym: string): string { + const d = new Date(`${ym}-01`); + d.setMonth(d.getMonth() + 1); + return d.toISOString().slice(0, 7) + '-01'; + } + // 甘特图数据:每个宿舍的入住时间线 async getGanttData(query?: { periodStart?: string; periodEnd?: string; building?: string }) { const qb = this.occRepo