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
This commit is contained in:
41
apps/server/src/dashboard/dashboard.controller.ts
Normal file
41
apps/server/src/dashboard/dashboard.controller.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { Controller, Get, Query, UseGuards } from '@nestjs/common';
|
||||
import { DashboardService } from './dashboard.service';
|
||||
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
|
||||
import { RequirePermission } from '../auth/decorators/permission.decorator';
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@RequirePermission('dashboard:view')
|
||||
@Controller('dashboard')
|
||||
export class DashboardController {
|
||||
constructor(private service: DashboardService) {}
|
||||
|
||||
@Get('stats')
|
||||
getStats() {
|
||||
return this.service.getStats();
|
||||
}
|
||||
|
||||
@Get('gantt')
|
||||
getGanttData(
|
||||
@Query('periodStart') periodStart?: string,
|
||||
@Query('periodEnd') periodEnd?: string,
|
||||
@Query('building') building?: string,
|
||||
) {
|
||||
return this.service.getGanttData({ periodStart, periodEnd, building });
|
||||
}
|
||||
|
||||
@Get('expense-stats')
|
||||
getExpenseStats(
|
||||
@Query('periodStart') periodStart?: string,
|
||||
@Query('periodEnd') periodEnd?: string,
|
||||
) {
|
||||
return this.service.getExpenseStats(periodStart, periodEnd);
|
||||
}
|
||||
|
||||
@Get('room-ranking')
|
||||
getRoomExpenseRanking(
|
||||
@Query('periodStart') periodStart?: string,
|
||||
@Query('periodEnd') periodEnd?: string,
|
||||
) {
|
||||
return this.service.getRoomExpenseRanking(periodStart, periodEnd);
|
||||
}
|
||||
}
|
||||
16
apps/server/src/dashboard/dashboard.module.ts
Normal file
16
apps/server/src/dashboard/dashboard.module.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/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';
|
||||
import { DashboardService } from './dashboard.service';
|
||||
import { DashboardController } from './dashboard.controller';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([Room, Student, Occupancy, Bill, RoomExpense])],
|
||||
controllers: [DashboardController],
|
||||
providers: [DashboardService],
|
||||
})
|
||||
export class DashboardModule {}
|
||||
108
apps/server/src/dashboard/dashboard.service.ts
Normal file
108
apps/server/src/dashboard/dashboard.service.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user