import { Injectable, NotFoundException } from '@nestjs/common' import { MembershipStatus, BookingStatus, UserRole } from '@mp-pilates/shared' import type { PaginatedData, UserProfileResponse, UserStatsResponse } from '@mp-pilates/shared' import { PrismaService } from '../prisma/prisma.service' @Injectable() export class UserService { constructor(private readonly prisma: PrismaService) {} async getProfile(userId: string): Promise { const user = await this.prisma.user.findUnique({ where: { id: userId }, include: { _count: { select: { memberships: { where: { status: MembershipStatus.ACTIVE }, }, }, }, }, }) if (!user) { throw new NotFoundException('User not found') } return { id: user.id, phone: user.phone, nickname: user.nickname, avatarUrl: user.avatarUrl, role: user.role as UserRole, activeMembershipCount: user._count.memberships, createdAt: user.createdAt.toISOString(), } } async updateProfile( userId: string, dto: { nickname?: string; avatarUrl?: string }, ): Promise { const updated = await this.prisma.user.update({ where: { id: userId }, data: { ...(dto.nickname !== undefined && { nickname: dto.nickname }), ...(dto.avatarUrl !== undefined && { avatarUrl: dto.avatarUrl }), }, include: { _count: { select: { memberships: { where: { status: MembershipStatus.ACTIVE }, }, }, }, }, }) return { id: updated.id, phone: updated.phone, nickname: updated.nickname, avatarUrl: updated.avatarUrl, role: updated.role as UserRole, activeMembershipCount: updated._count.memberships, createdAt: updated.createdAt.toISOString(), } } async getStats(userId: string): Promise { const completedBookings = await this.prisma.booking.findMany({ where: { userId, status: BookingStatus.COMPLETED, }, include: { timeSlot: { select: { date: true, startTime: true, endTime: true, }, }, }, }) const now = new Date() const monthStart = new Date(now.getFullYear(), now.getMonth(), 1) const monthEnd = new Date(now.getFullYear(), now.getMonth() + 1, 0, 23, 59, 59, 999) const monthBookings = completedBookings.filter((b) => { const slotDate = new Date(b.timeSlot.date) return slotDate >= monthStart && slotDate <= monthEnd }) const totalDays = new Set( completedBookings.map((b) => b.timeSlot.date.toISOString().split('T')[0]), ).size const monthDays = new Set( monthBookings.map((b) => b.timeSlot.date.toISOString().split('T')[0]), ).size const monthHours = monthBookings.reduce((sum, b) => { const [startH, startM] = b.timeSlot.startTime.split(':').map(Number) const [endH, endM] = b.timeSlot.endTime.split(':').map(Number) const durationMinutes = endH * 60 + endM - (startH * 60 + startM) return sum + durationMinutes / 60 }, 0) return { totalBookings: completedBookings.length, totalDays, monthBookings: monthBookings.length, monthDays, monthHours, } } // ─── Admin: paginated member list ───────────────────────────────────────── async getMembers( page: number, limit: number, search?: string, ): Promise> { const where = search ? { OR: [ { nickname: { contains: search, mode: 'insensitive' as const } }, { openid: { contains: search, mode: 'insensitive' as const } }, { phone: { contains: search } }, ], } : {} const [users, total] = await Promise.all([ this.prisma.user.findMany({ where, select: { id: true, openid: true, nickname: true, phone: true, avatarUrl: true, _count: { select: { bookings: true, }, }, }, orderBy: { createdAt: 'desc' }, skip: (page - 1) * limit, take: limit, }), this.prisma.user.count({ where }), ]) // Batch-fetch booking stats for the page of users const userIds = users.map((u) => u.id) const bookingStats = userIds.length ? await this.prisma.booking.groupBy({ by: ['userId', 'status'], where: { userId: { in: userIds } }, _count: { id: true }, }) : [] const statsMap = new Map() for (const stat of bookingStats) { const entry = statsMap.get(stat.userId) ?? { total: 0, completed: 0, cancelled: 0 } entry.total += stat._count.id if (stat.status === BookingStatus.COMPLETED) entry.completed += stat._count.id if (stat.status === BookingStatus.CANCELLED) entry.cancelled += stat._count.id statsMap.set(stat.userId, entry) } const items = users.map((u) => { const s = statsMap.get(u.id) ?? { total: 0, completed: 0, cancelled: 0 } return { userId: u.id, openid: u.openid, nickname: u.nickname, phone: u.phone, avatarUrl: u.avatarUrl, totalBookings: s.total, completedBookings: s.completed, cancelledBookings: s.cancelled, } }) return { items, total, page, limit } } }