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
This commit is contained in:
@@ -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],
|
||||
})
|
||||
|
||||
@@ -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<Occupancy>,
|
||||
@InjectRepository(Bill) private billRepo: Repository<Bill>,
|
||||
@InjectRepository(RoomExpense) private expRepo: Repository<RoomExpense>,
|
||||
@InjectRepository(Classroom) private classroomRepo: Repository<Classroom>,
|
||||
@InjectRepository(ClassSchedule) private scheduleRepo: Repository<ClassSchedule>,
|
||||
@InjectRepository(AttendanceRecord) private attendanceRepo: Repository<AttendanceRecord>,
|
||||
) {}
|
||||
|
||||
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<string, { total: number; present: number }>();
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user