feat: integrate classroom real-time occupancy with schedule and rental data
This commit is contained in:
@@ -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: '操作',
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user