fix(teaching-schedule): 当日课表展示所有状态,并支持点入课程详情
- 后端 getTeachingScheduleByDate 在查询当天时不再以 PENDING/CONFIRMED 过滤 timeSlot 也不限制 include.bookings 的状态,但仍保留 EXISTS 守卫过滤空 slot - 其他日期维持原「只看待上课」语义 - 抽出私有 formatLocalDate 工具方法,消除服务端 YYYY-MM-DD 构造的 3 处重复 - 前端 teaching-schedule 的 session 卡片可点击跳到 /pages/booking/detail?slotId= - 补全 student__status--completed / --cancelled 样式
This commit is contained in:
@@ -1138,7 +1138,7 @@ describe('BookingService', () => {
|
||||
},
|
||||
])
|
||||
|
||||
const result = await service.getTeachingScheduleByDate('2026-04-19')
|
||||
const result = await service.getTeachingScheduleByDate('2099-12-31')
|
||||
|
||||
expect(prisma.timeSlot.findMany).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
@@ -1158,7 +1158,7 @@ describe('BookingService', () => {
|
||||
expect(result).toEqual([
|
||||
{
|
||||
slotId: 'slot-01',
|
||||
date: '2026-04-19',
|
||||
date: '2099-12-31',
|
||||
startTime: '09:00',
|
||||
endTime: '10:00',
|
||||
bookedCount: 2,
|
||||
@@ -1175,7 +1175,7 @@ describe('BookingService', () => {
|
||||
},
|
||||
{
|
||||
slotId: 'slot-02',
|
||||
date: '2026-04-19',
|
||||
date: '2099-12-31',
|
||||
startTime: '11:00',
|
||||
endTime: '12:00',
|
||||
bookedCount: 1,
|
||||
@@ -1193,6 +1193,98 @@ describe('BookingService', () => {
|
||||
])
|
||||
})
|
||||
|
||||
it('returns all-status bookings when the date is today', async () => {
|
||||
const now = new Date()
|
||||
const today = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`
|
||||
|
||||
;(prisma.timeSlot.findMany as jest.Mock).mockResolvedValue([
|
||||
{
|
||||
id: 'slot-today',
|
||||
startTime: '10:00',
|
||||
endTime: '11:00',
|
||||
bookedCount: 2,
|
||||
capacity: 3,
|
||||
bookings: [
|
||||
{
|
||||
id: 'booking-completed',
|
||||
status: BookingStatus.COMPLETED,
|
||||
createdAt: new Date(`${today}T00:00:00Z`),
|
||||
user: { id: 'user-1', nickname: '完成', phone: '13800000001' },
|
||||
},
|
||||
{
|
||||
id: 'booking-no-show',
|
||||
status: BookingStatus.NO_SHOW,
|
||||
createdAt: new Date(`${today}T00:00:01Z`),
|
||||
user: { id: 'user-2', nickname: '未到', phone: null },
|
||||
},
|
||||
{
|
||||
id: 'booking-cancelled',
|
||||
status: BookingStatus.CANCELLED,
|
||||
createdAt: new Date(`${today}T00:00:02Z`),
|
||||
user: { id: 'user-3', nickname: '取消', phone: '13800000003' },
|
||||
},
|
||||
{
|
||||
id: 'booking-confirmed',
|
||||
status: BookingStatus.CONFIRMED,
|
||||
createdAt: new Date(`${today}T00:00:03Z`),
|
||||
user: { id: 'user-4', nickname: '确认', phone: null },
|
||||
},
|
||||
],
|
||||
},
|
||||
])
|
||||
|
||||
const result = await service.getTeachingScheduleByDate(today)
|
||||
|
||||
// 当日课表:timeSlot 仍用 EXISTS 守卫过滤完全没人预约的空 slot(不带状态条件),
|
||||
// 且 included bookings 不再限制状态。
|
||||
const callArg = (prisma.timeSlot.findMany as jest.Mock).mock.calls[0][0]
|
||||
expect(callArg.where).toEqual({
|
||||
date: expect.any(Date),
|
||||
bookings: { some: {} },
|
||||
})
|
||||
expect(callArg.include.bookings.where).toBeUndefined()
|
||||
expect(result).toEqual([
|
||||
{
|
||||
slotId: 'slot-today',
|
||||
date: today,
|
||||
startTime: '10:00',
|
||||
endTime: '11:00',
|
||||
bookedCount: 2,
|
||||
capacity: 3,
|
||||
students: [
|
||||
{
|
||||
bookingId: 'booking-completed',
|
||||
userId: 'user-1',
|
||||
nickname: '完成',
|
||||
phone: '13800000001',
|
||||
status: BookingStatus.COMPLETED,
|
||||
},
|
||||
{
|
||||
bookingId: 'booking-no-show',
|
||||
userId: 'user-2',
|
||||
nickname: '未到',
|
||||
phone: null,
|
||||
status: BookingStatus.NO_SHOW,
|
||||
},
|
||||
{
|
||||
bookingId: 'booking-cancelled',
|
||||
userId: 'user-3',
|
||||
nickname: '取消',
|
||||
phone: '13800000003',
|
||||
status: BookingStatus.CANCELLED,
|
||||
},
|
||||
{
|
||||
bookingId: 'booking-confirmed',
|
||||
userId: 'user-4',
|
||||
nickname: '确认',
|
||||
phone: null,
|
||||
status: BookingStatus.CONFIRMED,
|
||||
},
|
||||
],
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it('rejects invalid date input', async () => {
|
||||
await expect(service.getTeachingScheduleByDate('invalid-date')).rejects.toThrow(
|
||||
BadRequestException,
|
||||
|
||||
@@ -764,20 +764,22 @@ export class BookingService {
|
||||
throw new BadRequestException('Invalid date')
|
||||
}
|
||||
|
||||
// 当日的课表需要包含所有状态的预约:老师下课后,已核销 / 标记未到 / 已取消
|
||||
// 的预约不应从课表里消失,状态由前端标签呈现;其他日期维持「只看待上课」语义。
|
||||
const showAllStatuses = this.isLocalToday(date)
|
||||
const activeBookingFilter = {
|
||||
status: { in: [BookingStatus.PENDING_CONFIRMATION, BookingStatus.CONFIRMED] },
|
||||
}
|
||||
|
||||
const slots = await this.prisma.timeSlot.findMany({
|
||||
where: {
|
||||
date: dayStart,
|
||||
bookings: {
|
||||
some: {
|
||||
status: { in: [BookingStatus.PENDING_CONFIRMATION, BookingStatus.CONFIRMED] },
|
||||
},
|
||||
},
|
||||
// 仍用 EXISTS 守卫过滤掉完全没人预约的空 slot,但今天放开状态过滤。
|
||||
bookings: { some: showAllStatuses ? {} : activeBookingFilter },
|
||||
},
|
||||
include: {
|
||||
bookings: {
|
||||
where: {
|
||||
status: { in: [BookingStatus.PENDING_CONFIRMATION, BookingStatus.CONFIRMED] },
|
||||
},
|
||||
...(showAllStatuses ? {} : { where: activeBookingFilter }),
|
||||
include: {
|
||||
user: {
|
||||
select: {
|
||||
@@ -824,6 +826,14 @@ export class BookingService {
|
||||
})
|
||||
}
|
||||
|
||||
private isLocalToday(date: string): boolean {
|
||||
return date === this.formatLocalDate(new Date())
|
||||
}
|
||||
|
||||
private formatLocalDate(date: Date): string {
|
||||
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`
|
||||
}
|
||||
|
||||
// ─── Private Helpers ─────────────────────────────────────────────────────
|
||||
|
||||
private async fetchBookingWithRelations(bookingId: string): Promise<BookingWithRelations> {
|
||||
@@ -855,8 +865,7 @@ export class BookingService {
|
||||
}
|
||||
|
||||
const studio = await this.studioService.getInfo()
|
||||
const bookingDate = booking.timeSlot.date
|
||||
const dateLabel = `${bookingDate.getFullYear()}-${String(bookingDate.getMonth() + 1).padStart(2, '0')}-${String(bookingDate.getDate()).padStart(2, '0')}`
|
||||
const dateLabel = this.formatLocalDate(booking.timeSlot.date)
|
||||
|
||||
await this.subscriptionMessageService.sendBookingConfirmedMessage({
|
||||
openid: user.openid,
|
||||
@@ -894,8 +903,7 @@ export class BookingService {
|
||||
select: { nickname: true, phone: true },
|
||||
})
|
||||
const studio = await this.studioService.getInfo()
|
||||
const bookingDate = booking.timeSlot.date
|
||||
const dateLabel = `${bookingDate.getFullYear()}-${String(bookingDate.getMonth() + 1).padStart(2, '0')}-${String(bookingDate.getDate()).padStart(2, '0')}`
|
||||
const dateLabel = this.formatLocalDate(booking.timeSlot.date)
|
||||
const studentLabel = this.buildAdminBookingStudentLabel(student)
|
||||
await Promise.allSettled(
|
||||
admins
|
||||
|
||||
Reference in New Issue
Block a user