feat(app): 新增个人中心课表视图
This commit is contained in:
@@ -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,
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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> {
|
||||
|
||||
Reference in New Issue
Block a user