feat: 优化页面 UI 以及支持个人中心 练习足迹
This commit is contained in:
@@ -1542,4 +1542,44 @@ describe('BookingService', () => {
|
||||
expect(tx.booking.create).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
describe('getPracticeActivity', () => {
|
||||
afterEach(() => jest.restoreAllMocks())
|
||||
|
||||
it('uses China today across UTC midnight and counts scheduled dates without pagination', async () => {
|
||||
jest.spyOn(Date, 'now').mockReturnValue(Date.parse('2026-09-07T16:01:00Z'))
|
||||
;(prisma.booking.findMany as jest.Mock).mockResolvedValue([
|
||||
{ timeSlot: { date: new Date('2026-08-10T00:00:00Z') } },
|
||||
{ timeSlot: { date: new Date('2026-09-08T00:00:00Z') } },
|
||||
{ timeSlot: { date: new Date('2026-09-08T00:00:00Z') } },
|
||||
])
|
||||
const result = await service.getPracticeActivity(MOCK_USER_ID)
|
||||
expect(result.days).toHaveLength(30)
|
||||
expect(result.days[0]).toEqual({ date: '2026-08-10', count: 1 })
|
||||
expect(result.days[29]).toEqual({ date: '2026-09-08', count: 2 })
|
||||
expect(result.days[1]).toEqual({ date: '2026-08-11', count: 0 })
|
||||
expect(prisma.booking.findMany).toHaveBeenCalledWith({
|
||||
where: {
|
||||
userId: MOCK_USER_ID,
|
||||
status: BookingStatus.COMPLETED,
|
||||
timeSlot: { date: {
|
||||
gte: new Date('2026-08-10T00:00:00Z'),
|
||||
lt: new Date('2026-09-09T00:00:00Z'),
|
||||
} },
|
||||
},
|
||||
select: { timeSlot: { select: { date: true } } },
|
||||
})
|
||||
})
|
||||
|
||||
it('returns every day with zero counts across a leap-year boundary', async () => {
|
||||
jest.spyOn(Date, 'now').mockReturnValue(Date.parse('2024-03-01T01:00:00Z'))
|
||||
;(prisma.booking.findMany as jest.Mock).mockResolvedValue([])
|
||||
const result = await service.getPracticeActivity(MOCK_USER_ID)
|
||||
expect(result.days).toHaveLength(30)
|
||||
expect(result.days[0].date).toBe('2024-02-01')
|
||||
expect(result.days[28].date).toBe('2024-02-29')
|
||||
expect(result.days[29].date).toBe('2024-03-01')
|
||||
expect(result.days.every(day => day.count === 0)).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
@@ -42,6 +42,12 @@ export class BookingController {
|
||||
return this.bookingService.cancelBooking(userId, id)
|
||||
}
|
||||
|
||||
@Get('booking/my/activity')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
async getPracticeActivity(@CurrentUser('sub') userId: string) {
|
||||
return this.bookingService.getPracticeActivity(userId)
|
||||
}
|
||||
|
||||
@Get('booking/my/upcoming')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
async getUpcomingBookings(@CurrentUser('sub') userId: string) {
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
MembershipStatus,
|
||||
TimeSlotStatus,
|
||||
type TeachingScheduleSlot,
|
||||
type PracticeActivity,
|
||||
} from '@mp-pilates/shared'
|
||||
import { PrismaService } from '../prisma/prisma.service'
|
||||
import { MembershipService } from '../membership/membership.service'
|
||||
@@ -670,6 +671,34 @@ export class BookingService {
|
||||
|
||||
// ─── Get Upcoming Bookings ────────────────────────────────────────────────
|
||||
|
||||
async getPracticeActivity(userId: string): Promise<PracticeActivity> {
|
||||
// Slot dates are stored as UTC midnight date-only values. Determine today's
|
||||
// calendar date in China independently of the server's timezone.
|
||||
const dayMs = 86_400_000
|
||||
const today = new Date(Date.now() + 8 * 3_600_000).toISOString().slice(0, 10)
|
||||
const end = new Date(today + 'T00:00:00Z').getTime()
|
||||
const start = end - 29 * dayMs
|
||||
const bookings = await this.prisma.booking.findMany({
|
||||
where: {
|
||||
userId,
|
||||
status: BookingStatus.COMPLETED,
|
||||
timeSlot: { date: { gte: new Date(start), lt: new Date(end + dayMs) } },
|
||||
},
|
||||
select: { timeSlot: { select: { date: true } } },
|
||||
})
|
||||
const counts = new Map<string, number>()
|
||||
for (const booking of bookings) {
|
||||
const date = booking.timeSlot.date.toISOString().slice(0, 10)
|
||||
counts.set(date, (counts.get(date) ?? 0) + 1)
|
||||
}
|
||||
return {
|
||||
days: Array.from({ length: 30 }, (_, index) => {
|
||||
const date = new Date(start + index * dayMs).toISOString().slice(0, 10)
|
||||
return { date, count: counts.get(date) ?? 0 }
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
async getUpcomingBookings(userId: string): Promise<BookingWithRelations[]> {
|
||||
const today = new Date()
|
||||
today.setUTCHours(0, 0, 0, 0)
|
||||
|
||||
Reference in New Issue
Block a user