feat(dashboard): add classroom utilization stats endpoint and UI
This commit is contained in:
@@ -48,4 +48,9 @@ export class DashboardController {
|
||||
getClassroomOccupancy() {
|
||||
return this.service.getClassroomOccupancy();
|
||||
}
|
||||
|
||||
@Get('classroom-utilization')
|
||||
async getClassroomUtilization() {
|
||||
return this.service.getClassroomUtilizationStats();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -383,4 +383,72 @@ export class DashboardService {
|
||||
occupancy: Math.min(((sMap[c.id] || 0) + (rMap[c.id] || 0) * 3) / 7, 1),
|
||||
}));
|
||||
}
|
||||
|
||||
async getClassroomUtilizationStats() {
|
||||
const scopeIds = await this.scope.getScopeDepartmentIds();
|
||||
const totalClassrooms = await this.classroomRepo.count({
|
||||
where: await this.scope.filter({ status: Not('archived') }),
|
||||
});
|
||||
|
||||
const today = new Date().toISOString().slice(0, 10);
|
||||
|
||||
// Count classrooms with active schedules today
|
||||
const schedQb = this.scheduleRepo
|
||||
.createQueryBuilder('s')
|
||||
.select('COUNT(DISTINCT s.classroomId)', 'cnt')
|
||||
.where('s.status = :active', { active: 'active' })
|
||||
.andWhere('s.scheduleType = :type', { type: 'INTERNAL' })
|
||||
.andWhere('s.startDate <= :today AND s.endDate >= :today', { today });
|
||||
if (scopeIds) schedQb.andWhere('s.departmentId IN (:...scopeIds)', { scopeIds });
|
||||
const schedResult = await schedQb.getRawOne();
|
||||
|
||||
// Count classrooms with active rentals today
|
||||
const rentalQb = this.rentalRepo
|
||||
.createQueryBuilder('r')
|
||||
.select('COUNT(DISTINCT r.classroomId)', 'cnt')
|
||||
.where('r.status != :cancelled', { cancelled: 'cancelled' })
|
||||
.andWhere('r.startDate <= :today AND r.endDate >= :today', { today });
|
||||
if (scopeIds) rentalQb.andWhere('r.departmentId IN (:...scopeIds)', { scopeIds });
|
||||
const rentalResult = await rentalQb.getRawOne();
|
||||
|
||||
// Combine: use Set merge of both
|
||||
const combinedQb = this.scheduleRepo
|
||||
.createQueryBuilder('s')
|
||||
.select('s.classroomId')
|
||||
.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) combinedQb.andWhere('s.departmentId IN (:...scopeIds)', { scopeIds });
|
||||
const schedIds = await combinedQb.getRawMany();
|
||||
|
||||
const combinedRentalQb = this.rentalRepo
|
||||
.createQueryBuilder('r')
|
||||
.select('r.classroomId')
|
||||
.where('r.status != :cancelled', { cancelled: 'cancelled' })
|
||||
.andWhere('r.startDate <= :today AND r.endDate >= :today', { today })
|
||||
.groupBy('r.classroomId');
|
||||
if (scopeIds) combinedRentalQb.andWhere('r.departmentId IN (:...scopeIds)', { scopeIds });
|
||||
const rentalIds = await combinedRentalQb.getRawMany();
|
||||
|
||||
const allInUseIds = new Set([
|
||||
...schedIds.map((s) => s.classroomId),
|
||||
...rentalIds.map((r) => r.classroomId),
|
||||
]);
|
||||
|
||||
const scheduleCount = parseInt(schedResult?.cnt || '0', 10);
|
||||
const rentalCount = parseInt(rentalResult?.cnt || '0', 10);
|
||||
const inUseCount = allInUseIds.size;
|
||||
const utilizationRate = totalClassrooms > 0
|
||||
? ((inUseCount / totalClassrooms) * 100).toFixed(1)
|
||||
: '0';
|
||||
|
||||
return {
|
||||
totalClassrooms,
|
||||
inUseCount,
|
||||
utilizationRate,
|
||||
scheduleCount,
|
||||
rentalCount,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user