feat: integrate classroom real-time occupancy with schedule and rental data

This commit is contained in:
2026-07-06 01:14:58 +08:00
parent 3bcb7d41c0
commit 8453e62088
2 changed files with 92 additions and 5 deletions

View File

@@ -12,6 +12,7 @@ import {
Tag,
Popconfirm,
Upload,
Tooltip,
} from 'antd';
import {
PlusOutlined,
@@ -25,9 +26,18 @@ import PermissionButton from '../../components/PermissionButton';
const statusMap: Record<string, { text: string; color: string }> = {
available: { text: '可用', color: 'green' },
in_use: { text: '使用中', color: 'blue' },
maintenance: { text: '维护中', color: 'orange' },
archived: { text: '已归档', color: '#999' },
};
interface CurrentUsage {
type: 'schedule' | 'rental';
title: string;
startTime: string;
endTime: string;
}
const typeColor: Record<string, string> = {
: 'volcano',
: 'geekblue',
@@ -142,7 +152,14 @@ const ClassroomsPage: React.FC = () => {
{
title: '状态',
dataIndex: 'status',
render: (s: string) => <Tag color={statusMap[s]?.color}>{statusMap[s]?.text || s}</Tag>,
render: (s: string, record: { currentUsage?: CurrentUsage | null }) => {
const effectiveStatus = record.currentUsage ? 'in_use' : s;
return (
<Tooltip title={record.currentUsage ? `${record.currentUsage.title} (${record.currentUsage.startTime}-${record.currentUsage.endTime})` : undefined}>
<Tag color={statusMap[effectiveStatus]?.color}>{statusMap[effectiveStatus]?.text || s}</Tag>
</Tooltip>
);
},
},
{
title: '操作',

View File

@@ -22,13 +22,16 @@ export class ClassroomsService {
if (query?.building) where.building = query.building;
if (query?.roomType) where.roomType = query.roomType;
if (!query?.includeArchived) where.status = Not('archived');
return this.repo.find({ where: await this.scope.filter(where), order: { building: 'ASC', name: 'ASC' } });
const list = await this.repo.find({ where: await this.scope.filter(where), order: { building: 'ASC', name: 'ASC' } });
const usageMap = await this.getCurrentUsageForClassrooms(list.map((c) => c.id));
return list.map((c) => ({ ...c, currentUsage: usageMap.get(c.id) ?? null }));
}
async findOne(id: number) {
const cls = await this.repo.findOne({ where: { id } });
if (!cls) throw new NotFoundException('教室不存在');
return cls;
const usageMap = await this.getCurrentUsageForClassrooms([id]);
return { ...cls, currentUsage: usageMap.get(id) ?? null };
}
async create(dto: CreateClassroomDto) {
@@ -47,16 +50,83 @@ export class ClassroomsService {
async remove(id: number) {
await this.findOne(id);
await this.repo.update(id, { status: 'archived' } as any);
await this.repo.update(id, { status: 'archived' });
return { message: '已归档' };
}
async restore(id: number) {
await this.findOne(id);
await this.repo.update(id, { status: 'available' } as any);
await this.repo.update(id, { status: 'available' });
return this.repo.findOne({ where: { id } });
}
private async getCurrentUsageForClassrooms(classroomIds: number[]): Promise<Map<number, { type: 'schedule' | 'rental'; title: string; startTime: string; endTime: string }>> {
const result = new Map<number, { type: 'schedule' | 'rental'; title: string; startTime: string; endTime: string }>();
if (classroomIds.length === 0) return result;
const now = new Date();
const todayStr = now.toISOString().slice(0, 10);
const currentTime = now.toTimeString().slice(0, 5);
const weekDay = now.getDay() || 7;
const schedules = await this.scheduleRepo
.createQueryBuilder('s')
.leftJoin('Class', 'c', 'c.id = s.classId')
.select('s.classroomId', 'classroomId')
.addSelect('s.startTime', 'startTime')
.addSelect('s.endTime', 'endTime')
.addSelect('s.subject', 'subject')
.addSelect('c.name', 'className')
.where('s.classroomId IN (:...ids)', { ids: classroomIds })
.andWhere('s.status = :active', { active: 'active' })
.andWhere('s.scheduleType = :type', { type: 'INTERNAL' })
.andWhere('s.startDate <= :today', { today: todayStr })
.andWhere('s.endDate >= :today', { today: todayStr })
.andWhere('s.weekDay = :weekDay', { weekDay })
.andWhere('s.startTime <= :currentTime', { currentTime })
.andWhere('s.endTime >= :currentTime', { currentTime })
.getRawMany();
for (const s of schedules) {
const classroomId = Number(s.classroomId);
if (!result.has(classroomId)) {
result.set(classroomId, {
type: 'schedule',
title: `${s.className || ''} ${s.subject || ''}`.trim() || '内部课程',
startTime: String(s.startTime),
endTime: String(s.endTime),
});
}
}
const rentals = await this.rentalRepo
.createQueryBuilder('r')
.leftJoin('Tenant', 't', 't.id = r.tenantId')
.select('r.classroomId', 'classroomId')
.addSelect('r.startDate', 'startDate')
.addSelect('r.endDate', 'endDate')
.addSelect('t.name', 'tenantName')
.where('r.classroomId IN (:...ids)', { ids: classroomIds })
.andWhere('r.status != :cancelled', { cancelled: 'cancelled' })
.andWhere('r.startDate <= :today', { today: todayStr })
.andWhere('r.endDate >= :today', { today: todayStr })
.getRawMany();
for (const r of rentals) {
const classroomId = Number(r.classroomId);
if (!result.has(classroomId)) {
result.set(classroomId, {
type: 'rental',
title: r.tenantName ? `${r.tenantName} 租赁` : '外部租赁',
startTime: '00:00',
endTime: '23:59',
});
}
}
return result;
}
async batchImport(
rows: {
name: string;