287 lines
9.6 KiB
TypeScript
287 lines
9.6 KiB
TypeScript
import {
|
|
Controller,
|
|
Get,
|
|
Post,
|
|
Put,
|
|
Delete,
|
|
Param,
|
|
Body,
|
|
Query,
|
|
UseGuards,
|
|
Request,
|
|
Res,
|
|
UseInterceptors,
|
|
UploadedFile,
|
|
} from '@nestjs/common';
|
|
import { InjectRepository } from '@nestjs/typeorm';
|
|
import { Repository } from 'typeorm';
|
|
import { Student } from '../entities/student.entity';
|
|
import { FileInterceptor } from '@nestjs/platform-express';
|
|
import type { Response } from 'express';
|
|
import { OccupanciesService } from './occupancies.service';
|
|
import { NotificationsService } from '../notifications/notifications.service';
|
|
import { NotificationType } from '../entities/notification.entity';
|
|
import { CheckInDto, CheckOutDto, TransferRoomDto, BatchCheckOutDto } from './dto/occupancy.dto';
|
|
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
|
|
import { OperationLogsService } from '../operation-logs/operation-logs.service';
|
|
import { extractRequestInfo } from '../common/request-utils';
|
|
import { RequirePermission } from '../auth/decorators/permission.decorator';
|
|
import * as ExcelJS from 'exceljs';
|
|
import {
|
|
createOccupancyImportTemplateWorkbook,
|
|
parseOccupancyImportWorksheet,
|
|
} from './occupancy-import-template';
|
|
|
|
@UseGuards(JwtAuthGuard)
|
|
@Controller('occupancies')
|
|
export class OccupanciesController {
|
|
constructor(
|
|
private service: OccupanciesService,
|
|
private logService: OperationLogsService,
|
|
private readonly notificationsService: NotificationsService,
|
|
@InjectRepository(Student) private studentRepo: Repository<Student>,
|
|
) {}
|
|
|
|
@Get()
|
|
@RequirePermission('occupancy:view')
|
|
findAll(
|
|
@Query('roomId') roomId?: string,
|
|
@Query('studentId') studentId?: string,
|
|
@Query('active') active?: string,
|
|
) {
|
|
return this.service.findAll({
|
|
roomId: roomId ? +roomId : undefined,
|
|
studentId: studentId ? +studentId : undefined,
|
|
active: active === 'true',
|
|
});
|
|
}
|
|
|
|
@Post('batch-check-out')
|
|
@RequirePermission('occupancy:checkout')
|
|
async batchCheckOut(@Body() dto: BatchCheckOutDto, @Request() req: any) {
|
|
const { ipAddress, userAgent } = extractRequestInfo(req);
|
|
const result = await this.service.batchCheckOut(dto);
|
|
await this.logService.log({
|
|
userId: req.user?.id,
|
|
username: req.user?.username,
|
|
module: '入住管理',
|
|
action: '批量退宿',
|
|
detail: `退宿 ${dto.ids.length} 人,日期 ${dto.checkOutDate}`,
|
|
ipAddress,
|
|
userAgent,
|
|
});
|
|
return result;
|
|
}
|
|
|
|
@Post('check-in')
|
|
@RequirePermission('occupancy:checkin')
|
|
async checkIn(@Body() dto: CheckInDto, @Request() req: any) {
|
|
const { ipAddress, userAgent } = extractRequestInfo(req);
|
|
const result = await this.service.checkIn(dto);
|
|
await this.logService.log({
|
|
userId: req.user?.id,
|
|
username: req.user?.username,
|
|
module: '入住管理',
|
|
action: '办理入住',
|
|
targetId: result.id,
|
|
targetType: 'occupancy',
|
|
detail: `学生${dto.studentId} 入住房间${dto.roomId}`,
|
|
ipAddress,
|
|
userAgent,
|
|
});
|
|
// Send check_in notification
|
|
try {
|
|
const student = await this.studentRepo.findOne({ where: { id: dto.studentId } });
|
|
if (student?.userId) {
|
|
void this.notificationsService.create({
|
|
recipientIds: [student.userId],
|
|
type: NotificationType.CHECK_IN,
|
|
title: '入住通知',
|
|
content: `您已入住房间 #${dto.roomId}`,
|
|
});
|
|
}
|
|
} catch (_) {
|
|
/* don't block response */
|
|
}
|
|
return result;
|
|
}
|
|
|
|
@Put(':id/check-out')
|
|
@RequirePermission('occupancy:checkout')
|
|
async checkOut(@Param('id') id: string, @Body() dto: CheckOutDto, @Request() req: any) {
|
|
const { ipAddress, userAgent } = extractRequestInfo(req);
|
|
const result = await this.service.checkOut(+id, dto);
|
|
await this.logService.log({
|
|
userId: req.user?.id,
|
|
username: req.user?.username,
|
|
module: '入住管理',
|
|
action: '办理退宿',
|
|
targetId: +id,
|
|
targetType: 'occupancy',
|
|
ipAddress,
|
|
userAgent,
|
|
});
|
|
// Send check_out notification
|
|
try {
|
|
const student = await this.studentRepo.findOne({ where: { id: result.studentId } });
|
|
if (student?.userId) {
|
|
void this.notificationsService.create({
|
|
recipientIds: [student.userId],
|
|
type: NotificationType.CHECK_OUT,
|
|
title: '退宿通知',
|
|
content: `您已退宿房间 #${result.roomId}`,
|
|
});
|
|
}
|
|
} catch (_) {
|
|
/* don't block response */
|
|
}
|
|
return result;
|
|
}
|
|
|
|
@Put(':id/transfer')
|
|
@RequirePermission('occupancy:transfer')
|
|
async transferRoom(@Param('id') id: string, @Body() dto: TransferRoomDto, @Request() req: any) {
|
|
const { ipAddress, userAgent } = extractRequestInfo(req);
|
|
const result = await this.service.transferRoom(+id, dto);
|
|
await this.logService.log({
|
|
userId: req.user?.id,
|
|
username: req.user?.username,
|
|
module: '入住管理',
|
|
action: '调换宿舍',
|
|
targetId: +id,
|
|
targetType: 'occupancy',
|
|
detail: `换到房间${dto.newRoomId}`,
|
|
ipAddress,
|
|
userAgent,
|
|
});
|
|
return result;
|
|
}
|
|
|
|
@Delete(':id')
|
|
@RequirePermission('occupancy:view')
|
|
async remove(@Param('id') id: string, @Request() req: any) {
|
|
const { ipAddress, userAgent } = extractRequestInfo(req);
|
|
const result = await this.service.remove(+id);
|
|
await this.logService.log({
|
|
userId: req.user?.id,
|
|
username: req.user?.username,
|
|
module: '入住管理',
|
|
action: '删除入住记录',
|
|
targetId: +id,
|
|
targetType: 'occupancy',
|
|
ipAddress,
|
|
userAgent,
|
|
});
|
|
return result;
|
|
}
|
|
|
|
@Post('batch-delete')
|
|
@RequirePermission('occupancy:view')
|
|
async batchRemove(@Body() body: { ids: number[] }, @Request() req: any) {
|
|
const { ipAddress, userAgent } = extractRequestInfo(req);
|
|
const result = await this.service.batchRemove(body.ids || []);
|
|
await this.logService.log({
|
|
userId: req.user?.id,
|
|
username: req.user?.username,
|
|
module: '入住管理',
|
|
action: '批量删除入住记录',
|
|
detail: `IDs: ${(body.ids || []).join(',')}`,
|
|
ipAddress,
|
|
userAgent,
|
|
});
|
|
return result;
|
|
}
|
|
|
|
@Get('export')
|
|
@RequirePermission('occupancy:view')
|
|
async exportExcel(@Query('active') active?: string, @Res() res?: Response) {
|
|
const records = await this.service.findAll({ active: active === 'true' });
|
|
const workbook = new ExcelJS.Workbook();
|
|
const ws = workbook.addWorksheet('入住记录');
|
|
ws.columns = [
|
|
{ header: '宿舍号', key: 'roomNumber', width: 12 },
|
|
{ header: '楼栋', key: 'building', width: 12 },
|
|
{ header: '学生姓名', key: 'studentName', width: 12 },
|
|
{ header: '性别', key: 'gender', width: 8 },
|
|
{ header: '电话', key: 'phone', width: 18 },
|
|
{ header: '学号/身份证', key: 'idNumber', width: 22 },
|
|
{ header: '所属机构', key: 'organization', width: 18 },
|
|
{ header: '负责人/班主任', key: 'supervisor', width: 15 },
|
|
{ header: '入住日期', key: 'checkInDate', width: 14 },
|
|
{ header: '退宿日期', key: 'checkOutDate', width: 14 },
|
|
{ header: '计费起始', key: 'billingStartDate', width: 14 },
|
|
{ header: '计费截止', key: 'billingEndDate', width: 14 },
|
|
{ header: '退宿原因', key: 'checkOutReason', width: 12 },
|
|
];
|
|
ws.getRow(1).font = { bold: true };
|
|
ws.getRow(1).fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFE0E0E0' } };
|
|
for (const r of records) {
|
|
ws.addRow({
|
|
roomNumber: r.room?.roomNumber || '',
|
|
building: r.room?.building || '',
|
|
studentName: r.student?.name || '',
|
|
gender: r.student?.gender || '',
|
|
phone: r.student?.phone || '',
|
|
idNumber: r.student?.idNumber || '',
|
|
organization: r.student?.organization?.name || '',
|
|
supervisor: r.student?.supervisor || '',
|
|
checkInDate: r.checkInDate || '',
|
|
checkOutDate: r.checkOutDate || '',
|
|
billingStartDate: r.billingStartDate || '',
|
|
billingEndDate: r.billingEndDate || '',
|
|
checkOutReason: r.checkOutReason || '',
|
|
});
|
|
}
|
|
res!.setHeader(
|
|
'Content-Type',
|
|
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
);
|
|
res!.setHeader('Content-Disposition', 'attachment; filename=occupancies.xlsx');
|
|
await workbook.xlsx.write(res!);
|
|
res!.end();
|
|
}
|
|
|
|
@Get('template')
|
|
@RequirePermission('occupancy:view')
|
|
async downloadTemplate(@Res() res: Response) {
|
|
const workbook = createOccupancyImportTemplateWorkbook();
|
|
res.setHeader(
|
|
'Content-Type',
|
|
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
);
|
|
res.setHeader('Content-Disposition', 'attachment; filename=checkin_template.xlsx');
|
|
await workbook.xlsx.write(res);
|
|
res.end();
|
|
}
|
|
|
|
@Post('import')
|
|
@RequirePermission('occupancy:checkin')
|
|
@UseInterceptors(FileInterceptor('file'))
|
|
async importCheckIn(
|
|
@UploadedFile() file: Express.Multer.File,
|
|
@Request() req: any,
|
|
@Query('autoDeposit') autoDeposit?: string,
|
|
@Query('depositAmount') depositAmount?: string,
|
|
) {
|
|
const { ipAddress, userAgent } = extractRequestInfo(req);
|
|
const workbook = new ExcelJS.Workbook();
|
|
await workbook.xlsx.load(file.buffer as any);
|
|
const ws = workbook.worksheets[0];
|
|
const rows = parseOccupancyImportWorksheet(ws);
|
|
const result = await this.service.batchImportCheckIn(rows, {
|
|
autoDeposit: autoDeposit === 'true',
|
|
depositAmount: depositAmount ? +depositAmount : undefined,
|
|
});
|
|
await this.logService.log({
|
|
userId: req.user?.id,
|
|
username: req.user?.username,
|
|
module: '入住管理',
|
|
action: '批量导入入住',
|
|
detail: result.message,
|
|
ipAddress,
|
|
userAgent,
|
|
});
|
|
return result;
|
|
}
|
|
}
|