forked from wangziqi/gongxue-base
feat: integrate CampusScope.filter() into all business services (12 services + 12 modules)
This commit is contained in:
@@ -38,4 +38,14 @@ export class DashboardController {
|
||||
) {
|
||||
return this.service.getRoomExpenseRanking(periodStart, periodEnd);
|
||||
}
|
||||
}
|
||||
|
||||
@Get('class-attendance-ranking')
|
||||
getClassAttendanceRanking() {
|
||||
return this.service.getClassAttendanceRanking();
|
||||
}
|
||||
|
||||
@Get('classroom-occupancy')
|
||||
getClassroomOccupancy() {
|
||||
return this.service.getClassroomOccupancy();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,10 +13,11 @@ import { Deposit } from '../entities/deposit.entity';
|
||||
import { ClassroomRental } from '../entities/classroom-rental.entity';
|
||||
import { ClassTeacher } from '../entities/class-teacher.entity';
|
||||
import { DashboardService } from './dashboard.service';
|
||||
import { DepartmentsModule } from '../departments/departments.module';
|
||||
import { DashboardController } from './dashboard.controller';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([Room, Student, Occupancy, Bill, RoomExpense, Classroom, ClassSchedule, AttendanceRecord, Class, Deposit, ClassroomRental, ClassTeacher])],
|
||||
imports: [TypeOrmModule.forFeature([Room, Student, Occupancy, Bill, RoomExpense, Classroom, ClassSchedule, AttendanceRecord, Class, Deposit, ClassroomRental, ClassTeacher]), DepartmentsModule],
|
||||
controllers: [DashboardController],
|
||||
providers: [DashboardService],
|
||||
})
|
||||
|
||||
@@ -13,9 +13,11 @@ import { Class } from '../entities/class.entity';
|
||||
import { Deposit } from '../entities/deposit.entity';
|
||||
import { ClassroomRental } from '../entities/classroom-rental.entity';
|
||||
import { ClassTeacher } from '../entities/class-teacher.entity';
|
||||
import { CampusScope } from '../common/campus-scope';
|
||||
|
||||
@Injectable()
|
||||
export class DashboardService {
|
||||
|
||||
constructor(
|
||||
@InjectRepository(Room) private roomRepo: Repository<Room>,
|
||||
@InjectRepository(Student) private studentRepo: Repository<Student>,
|
||||
@@ -29,54 +31,60 @@ export class DashboardService {
|
||||
@InjectRepository(Deposit) private depositRepo: Repository<Deposit>,
|
||||
@InjectRepository(ClassroomRental) private rentalRepo: Repository<ClassroomRental>,
|
||||
@InjectRepository(ClassTeacher) private classTeacherRepo: Repository<ClassTeacher>,
|
||||
private readonly scope: CampusScope,
|
||||
) {}
|
||||
|
||||
async getStats() {
|
||||
const today = new Date();
|
||||
const todayStr = today.toISOString().slice(0, 10);
|
||||
const currentMonth = todayStr.slice(0, 7); // YYYY-MM
|
||||
const scopeIds = await this.scope.getScopeDepartmentIds();
|
||||
|
||||
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
|
||||
const totalRooms = await this.roomRepo.count({ where: await this.scope.filter({ status: Not('archived') }) });
|
||||
const totalStudents = await this.studentRepo.count({ where: await this.scope.filter({ status: 'active' }) });
|
||||
const occupiedBeds = await this.occRepo.count({ where: await this.scope.filter({ checkOutDate: IsNull() }) });
|
||||
const capQb = this.roomRepo
|
||||
.createQueryBuilder('r')
|
||||
.select('SUM(r.capacity)', 'total')
|
||||
.where('r.status != :archived', { archived: 'archived' })
|
||||
.getRawOne();
|
||||
.where('r.status != :archived', { archived: 'archived' });
|
||||
if (scopeIds) capQb.andWhere('r.departmentId IN (:...scopeIds)', { scopeIds });
|
||||
const totalCapacity = await capQb.getRawOne();
|
||||
const cap = totalCapacity?.total || 0;
|
||||
const occupancyRate = cap > 0 ? ((occupiedBeds / cap) * 100).toFixed(1) : 0;
|
||||
|
||||
const billStats = await this.billRepo
|
||||
const billStatsQb = this.billRepo
|
||||
.createQueryBuilder('b')
|
||||
.select('b.status', 'status')
|
||||
.addSelect('COUNT(*)', 'count')
|
||||
.addSelect('SUM(b.totalAmount)', 'total')
|
||||
.groupBy('b.status')
|
||||
.getRawMany();
|
||||
.groupBy('b.status');
|
||||
if (scopeIds) billStatsQb.andWhere('b.departmentId IN (:...scopeIds)', { scopeIds });
|
||||
const billStats = await billStatsQb.getRawMany();
|
||||
|
||||
// New fields
|
||||
const classroomCount = await this.classroomRepo.count();
|
||||
const classroomCount = await this.classroomRepo.count({ where: await this.scope.filter({}) });
|
||||
|
||||
const occResult = await this.scheduleRepo
|
||||
const occQb = 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();
|
||||
.andWhere('s.endDate >= :today', { today: todayStr });
|
||||
if (scopeIds) occQb.andWhere('s.departmentId IN (:...scopeIds)', { scopeIds });
|
||||
const occResult = await occQb.getRawOne();
|
||||
const occupiedClassrooms = parseInt(occResult?.cnt || '0', 10);
|
||||
const classroomOccupancyRate = classroomCount > 0
|
||||
? ((occupiedClassrooms / classroomCount) * 100).toFixed(1)
|
||||
: 0;
|
||||
|
||||
const attTodayStats = await this.attendanceRepo
|
||||
const attTodayQb = this.attendanceRepo
|
||||
.createQueryBuilder('a')
|
||||
.select('a.status', 'status')
|
||||
.addSelect('COUNT(*)', 'count')
|
||||
.where('a.attendanceDate = :today', { today: todayStr })
|
||||
.groupBy('a.status')
|
||||
.getRawMany();
|
||||
.groupBy('a.status');
|
||||
if (scopeIds) attTodayQb.andWhere('a.departmentId IN (:...scopeIds)', { scopeIds });
|
||||
const attTodayStats = await attTodayQb.getRawMany();
|
||||
const todayTotal = attTodayStats.reduce((sum, r) => sum + parseInt(r.count, 10), 0);
|
||||
const todayPresent = attTodayStats
|
||||
.filter((r) => r.status === 'present')
|
||||
@@ -85,59 +93,62 @@ export class DashboardService {
|
||||
? ((todayPresent / todayTotal) * 100).toFixed(1)
|
||||
: 0;
|
||||
|
||||
const incomeResult = await this.billRepo
|
||||
const incomeQb = 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();
|
||||
.andWhere('b.periodStart < :end', { end: this.nextMonth(currentMonth) });
|
||||
if (scopeIds) incomeQb.andWhere('b.departmentId IN (:...scopeIds)', { scopeIds });
|
||||
const incomeResult = await incomeQb.getRawOne();
|
||||
const monthlyIncome = parseFloat(incomeResult?.total || '0');
|
||||
|
||||
const attendanceTrend = await this.getAttendanceTrend(todayStr);
|
||||
const incomeTrend = await this.getIncomeTrend(currentMonth);
|
||||
|
||||
// --- New stats ---
|
||||
const classCount = await this.classRepo.count();
|
||||
const classCount = await this.classRepo.count({ where: await this.scope.filter({}) });
|
||||
|
||||
// classTeacherRepo does not have departmentId — skip scope filtering
|
||||
const teacherResult = await this.classTeacherRepo
|
||||
.createQueryBuilder('ct')
|
||||
.select('COUNT(DISTINCT ct.userId)', 'cnt')
|
||||
.getRawOne();
|
||||
const teacherCount = parseInt(teacherResult?.cnt || '0', 10);
|
||||
|
||||
const pendingResult = await this.depositRepo
|
||||
const pendingQb = this.depositRepo
|
||||
.createQueryBuilder('d')
|
||||
.select('SUM(d.amount)', 'total')
|
||||
.where('d.status = :paid', { paid: 'paid' })
|
||||
.andWhere('d.refundStatus IS NULL')
|
||||
.getRawOne();
|
||||
.andWhere('d.refundStatus IS NULL');
|
||||
if (scopeIds) pendingQb.andWhere('d.departmentId IN (:...scopeIds)', { scopeIds });
|
||||
const pendingResult = await pendingQb.getRawOne();
|
||||
const pendingDeposits = parseFloat(pendingResult?.total || '0');
|
||||
|
||||
const activeRentals = await this.rentalRepo.count({ where: { endDate: MoreThanOrEqual(todayStr) } });
|
||||
const activeRentals = await this.rentalRepo.count({ where: await this.scope.filter({ endDate: MoreThanOrEqual(todayStr) }) });
|
||||
|
||||
const occupancyByBuilding = await this.occRepo
|
||||
const occByBldQb = this.occRepo
|
||||
.createQueryBuilder('o')
|
||||
.leftJoin('o.room', 'r')
|
||||
.select('r.building', 'building')
|
||||
.addSelect('COUNT(*)', 'count')
|
||||
.where('o.checkOutDate IS NULL')
|
||||
.groupBy('r.building')
|
||||
.getRawMany();
|
||||
.where('o.checkOutDate IS NULL');
|
||||
if (scopeIds) occByBldQb.andWhere('r.departmentId IN (:...scopeIds)', { scopeIds });
|
||||
const occupancyByBuilding = await occByBldQb.groupBy('r.building').getRawMany();
|
||||
|
||||
const attendanceByStatus = attTodayStats.reduce((acc, r) => {
|
||||
acc[r.status] = parseInt(r.count, 10);
|
||||
return acc;
|
||||
}, {} as Record<string, number>);
|
||||
|
||||
const expenseByType = await this.expRepo
|
||||
const expByTypeQb = this.expRepo
|
||||
.createQueryBuilder('e')
|
||||
.select('e.expenseType', 'type')
|
||||
.addSelect('SUM(e.amount)', 'total')
|
||||
.where('e.periodStart >= :start', { start: `${currentMonth}-01` })
|
||||
.andWhere('e.periodEnd <= :end', { end: this.nextMonth(currentMonth) })
|
||||
.groupBy('e.expenseType')
|
||||
.getRawMany();
|
||||
.andWhere('e.periodEnd <= :end', { end: this.nextMonth(currentMonth) });
|
||||
if (scopeIds) expByTypeQb.andWhere('e.departmentId IN (:...scopeIds)', { scopeIds });
|
||||
const expenseByType = await expByTypeQb.groupBy('e.expenseType').getRawMany();
|
||||
|
||||
return {
|
||||
totalRooms,
|
||||
@@ -227,6 +238,7 @@ export class DashboardService {
|
||||
|
||||
// 甘特图数据:每个宿舍的入住时间线
|
||||
async getGanttData(query?: { periodStart?: string; periodEnd?: string; building?: string }) {
|
||||
const scopeIds = await this.scope.getScopeDepartmentIds();
|
||||
const qb = this.occRepo
|
||||
.createQueryBuilder('o')
|
||||
.leftJoinAndSelect('o.student', 'student')
|
||||
@@ -235,6 +247,8 @@ export class DashboardService {
|
||||
.orderBy('room.roomNumber', 'ASC')
|
||||
.addOrderBy('o.checkInDate', 'ASC');
|
||||
|
||||
if (scopeIds) qb.andWhere('room.departmentId IN (:...scopeIds)', { scopeIds });
|
||||
|
||||
if (query?.building) {
|
||||
qb.andWhere('room.building = :building', { building: query.building });
|
||||
}
|
||||
@@ -248,7 +262,7 @@ export class DashboardService {
|
||||
const records = await qb.getMany();
|
||||
|
||||
// 按宿舍分组
|
||||
const roomMap = new Map<string, any[]>();
|
||||
const roomMap = new Map<string, Record<string, unknown>[]>();
|
||||
for (const r of records) {
|
||||
const key = r.room?.roomNumber || String(r.roomId);
|
||||
if (!roomMap.has(key)) roomMap.set(key, []);
|
||||
@@ -267,14 +281,15 @@ export class DashboardService {
|
||||
occupancies,
|
||||
}));
|
||||
}
|
||||
|
||||
// 费用统计
|
||||
async getExpenseStats(periodStart?: string, periodEnd?: string) {
|
||||
const scopeIds = await this.scope.getScopeDepartmentIds();
|
||||
const qb = this.expRepo
|
||||
.createQueryBuilder('e')
|
||||
.select('e.expenseType', 'type')
|
||||
.addSelect('SUM(e.amount)', 'total')
|
||||
.groupBy('e.expenseType');
|
||||
if (scopeIds) qb.andWhere('e.departmentId IN (:...scopeIds)', { scopeIds });
|
||||
if (periodStart) qb.andWhere('e.periodStart >= :ps', { ps: periodStart });
|
||||
if (periodEnd) qb.andWhere('e.periodEnd <= :pe', { pe: periodEnd });
|
||||
return qb.getRawMany();
|
||||
@@ -282,6 +297,7 @@ export class DashboardService {
|
||||
|
||||
// 各宿舍费用排行
|
||||
async getRoomExpenseRanking(periodStart?: string, periodEnd?: string) {
|
||||
const scopeIds = await this.scope.getScopeDepartmentIds();
|
||||
const qb = this.expRepo
|
||||
.createQueryBuilder('e')
|
||||
.leftJoin('e.room', 'room')
|
||||
@@ -291,8 +307,80 @@ export class DashboardService {
|
||||
.groupBy('e.roomId')
|
||||
.orderBy('total', 'DESC')
|
||||
.limit(20);
|
||||
if (scopeIds) qb.andWhere('e.departmentId IN (:...scopeIds)', { scopeIds });
|
||||
if (periodStart) qb.andWhere('e.periodStart >= :ps', { ps: periodStart });
|
||||
if (periodEnd) qb.andWhere('e.periodEnd <= :pe', { pe: periodEnd });
|
||||
return qb.getRawMany();
|
||||
}
|
||||
}
|
||||
|
||||
// 班级考勤排行
|
||||
async getClassAttendanceRanking() {
|
||||
const scopeIds = await this.scope.getScopeDepartmentIds();
|
||||
const qb = this.attendanceRepo
|
||||
.createQueryBuilder('a')
|
||||
.leftJoin('a.class', 'class')
|
||||
.select('class.id', 'classId')
|
||||
.addSelect('class.name', 'className')
|
||||
.addSelect('a.status', 'status')
|
||||
.addSelect('COUNT(*)', 'count')
|
||||
.groupBy('class.id').addGroupBy('class.name').addGroupBy('a.status');
|
||||
if (scopeIds) qb.andWhere('a.departmentId IN (:...scopeIds)', { scopeIds });
|
||||
const raw = await qb.getRawMany();
|
||||
|
||||
const classMap = new Map<number, { className: string; present: number; total: number }>();
|
||||
for (const r of raw) {
|
||||
if (!r.classId) continue;
|
||||
if (!classMap.has(Number(r.classId))) classMap.set(Number(r.classId), { className: r.className, present: 0, total: 0 });
|
||||
const entry = classMap.get(Number(r.classId))!;
|
||||
const n = parseInt(r.count, 10);
|
||||
entry.total += n;
|
||||
if (r.status === 'present') entry.present += n;
|
||||
}
|
||||
|
||||
const ranked = Array.from(classMap.values())
|
||||
.map(e => ({ ...e, rate: e.total > 0 ? parseFloat(((e.present / e.total) * 100).toFixed(1)) : 0 }))
|
||||
.sort((a, b) => b.rate - a.rate);
|
||||
|
||||
return { top: ranked.slice(0, 5), bottom: ranked.slice(-5).reverse() };
|
||||
}
|
||||
|
||||
async getClassroomOccupancy() {
|
||||
const scopeIds = await this.scope.getScopeDepartmentIds();
|
||||
const classrooms = await this.classroomRepo.find({
|
||||
where: await this.scope.filter({ status: Not('archived') }),
|
||||
order: { building: 'ASC', name: 'ASC' },
|
||||
});
|
||||
const today = new Date().toISOString().slice(0, 10);
|
||||
const schedQb = this.scheduleRepo
|
||||
.createQueryBuilder('s')
|
||||
.select('s.classroomId', 'classroomId')
|
||||
.addSelect('COUNT(DISTINCT s.weekDay)', 'weekDays')
|
||||
.where('s.status = :active', { active: 'active' })
|
||||
.andWhere('s.scheduleType = :type', { type: 'INTERNAL' })
|
||||
.andWhere('s.startDate <= :today AND s.endDate >= :today', { today })
|
||||
.groupBy('s.classroomId');
|
||||
if (scopeIds) schedQb.andWhere('s.departmentId IN (:...scopeIds)', { scopeIds });
|
||||
const schedules = await schedQb.getRawMany();
|
||||
const rentalQb = this.rentalRepo
|
||||
.createQueryBuilder('r')
|
||||
.select('r.classroomId', 'classroomId')
|
||||
.addSelect('COUNT(*)', 'rentalCount')
|
||||
.where('r.status != :cancelled', { cancelled: 'cancelled' })
|
||||
.andWhere('r.startDate <= :today AND r.endDate >= :today', { today })
|
||||
.groupBy('r.classroomId');
|
||||
if (scopeIds) rentalQb.andWhere('r.departmentId IN (:...scopeIds)', { scopeIds });
|
||||
const rentals = await rentalQb.getRawMany();
|
||||
const sMap: Record<number, number> = {};
|
||||
const rMap: Record<number, number> = {};
|
||||
for (const s of schedules) sMap[s.classroomId] = parseInt(s.weekDays, 10);
|
||||
for (const r of rentals) rMap[r.classroomId] = parseInt(r.rentalCount, 10);
|
||||
return classrooms.map((c) => ({
|
||||
name: c.name,
|
||||
building: c.building || '',
|
||||
capacity: c.capacity,
|
||||
scheduleDays: sMap[c.id] || 0,
|
||||
rentalCount: rMap[c.id] || 0,
|
||||
occupancy: Math.min(((sMap[c.id] || 0) + (rMap[c.id] || 0) * 3) / 7, 1),
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user