feat(app): 新增个人中心课表视图

This commit is contained in:
richarjiang
2026-04-19 22:23:23 +08:00
parent 9575210b06
commit bd3d519b4f
17 changed files with 998 additions and 29 deletions

View File

@@ -173,6 +173,7 @@ describe('BookingService', () => {
},
timeSlot: {
findUnique: jest.fn(),
findMany: jest.fn(),
update: jest.fn(),
},
membership: {
@@ -903,4 +904,101 @@ describe('BookingService', () => {
)
})
})
describe('getTeachingScheduleByDate', () => {
it('returns sorted slots with active students only', async () => {
;(prisma.timeSlot.findMany as jest.Mock).mockResolvedValue([
{
id: 'slot-02',
startTime: '11:00',
endTime: '12:00',
bookedCount: 1,
capacity: 1,
bookings: [
{
id: 'booking-02',
status: BookingStatus.CONFIRMED,
createdAt: new Date('2026-04-19T01:00:00Z'),
user: { id: 'user-02', nickname: '李四', phone: '13800000000' },
},
],
},
{
id: 'slot-01',
startTime: '09:00',
endTime: '10:00',
bookedCount: 2,
capacity: 2,
bookings: [
{
id: 'booking-01',
status: BookingStatus.PENDING_CONFIRMATION,
createdAt: new Date('2026-04-19T00:00:00Z'),
user: { id: 'user-01', nickname: '张三', phone: null },
},
],
},
])
const result = await service.getTeachingScheduleByDate('2026-04-19')
expect(prisma.timeSlot.findMany).toHaveBeenCalledWith(
expect.objectContaining({
where: expect.objectContaining({
bookings: {
some: {
status: { in: [BookingStatus.PENDING_CONFIRMATION, BookingStatus.CONFIRMED] },
},
},
}),
orderBy: [
{ startTime: 'asc' },
{ endTime: 'asc' },
],
}),
)
expect(result).toEqual([
{
slotId: 'slot-01',
date: '2026-04-19',
startTime: '09:00',
endTime: '10:00',
bookedCount: 2,
capacity: 2,
students: [
{
bookingId: 'booking-01',
userId: 'user-01',
nickname: '张三',
phone: null,
status: BookingStatus.PENDING_CONFIRMATION,
},
],
},
{
slotId: 'slot-02',
date: '2026-04-19',
startTime: '11:00',
endTime: '12:00',
bookedCount: 1,
capacity: 1,
students: [
{
bookingId: 'booking-02',
userId: 'user-02',
nickname: '李四',
phone: '13800000000',
status: BookingStatus.CONFIRMED,
},
],
},
])
})
it('rejects invalid date input', async () => {
await expect(service.getTeachingScheduleByDate('invalid-date')).rejects.toThrow(
BadRequestException,
)
})
})
})

View File

@@ -1,4 +1,5 @@
import {
BadRequestException,
Body,
Controller,
Get,
@@ -91,6 +92,16 @@ export class BookingController {
)
}
@Get('admin/teaching-schedule')
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles(UserRole.ADMIN)
async getTeachingSchedule(@Query('date') date?: string) {
if (!date) {
throw new BadRequestException('date is required')
}
return this.bookingService.getTeachingScheduleByDate(date)
}
@Put('booking/:id/confirm')
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles(UserRole.ADMIN)

View File

@@ -6,7 +6,13 @@ import {
NotFoundException,
} from '@nestjs/common'
import { Booking, Membership, TimeSlot, BookingStatusHistory } from '@prisma/client'
import { BookingStatus, CardTypeCategory, MembershipStatus, TimeSlotStatus } from '@mp-pilates/shared'
import {
BookingStatus,
CardTypeCategory,
MembershipStatus,
TimeSlotStatus,
type TeachingScheduleSlot,
} from '@mp-pilates/shared'
import { PrismaService } from '../prisma/prisma.service'
import { MembershipService } from '../membership/membership.service'
import { StudioService } from '../studio/studio.service'
@@ -582,6 +588,72 @@ export class BookingService {
}
}
async getTeachingScheduleByDate(date: string): Promise<TeachingScheduleSlot[]> {
const dayStart = new Date(`${date}T00:00:00.000Z`)
if (Number.isNaN(dayStart.getTime())) {
throw new BadRequestException('Invalid date')
}
const slots = await this.prisma.timeSlot.findMany({
where: {
date: dayStart,
bookings: {
some: {
status: { in: [BookingStatus.PENDING_CONFIRMATION, BookingStatus.CONFIRMED] },
},
},
},
include: {
bookings: {
where: {
status: { in: [BookingStatus.PENDING_CONFIRMATION, BookingStatus.CONFIRMED] },
},
include: {
user: {
select: {
id: true,
nickname: true,
phone: true,
},
},
},
orderBy: [
{ status: 'asc' },
{ createdAt: 'asc' },
],
},
},
orderBy: [
{ startTime: 'asc' },
{ endTime: 'asc' },
],
})
return slots
.map((slot) => ({
slotId: slot.id,
date,
startTime: slot.startTime,
endTime: slot.endTime,
bookedCount: slot.bookedCount,
capacity: slot.capacity,
students: slot.bookings.map((booking) => ({
bookingId: booking.id,
userId: booking.user.id,
nickname: booking.user.nickname,
phone: booking.user.phone,
status: booking.status as BookingStatus,
})),
}))
.sort((a, b) => {
const byStart = a.startTime.localeCompare(b.startTime)
if (byStart !== 0) {
return byStart
}
return a.endTime.localeCompare(b.endTime)
})
}
// ─── Private Helpers ─────────────────────────────────────────────────────
private async fetchBookingWithRelations(bookingId: string): Promise<BookingWithRelations> {