diff --git a/packages/app/src/components/ProfileMenu.vue b/packages/app/src/components/ProfileMenu.vue index 45c7734..6b2fedd 100644 --- a/packages/app/src/components/ProfileMenu.vue +++ b/packages/app/src/components/ProfileMenu.vue @@ -82,15 +82,25 @@ const menuItems = computed(() => { badge: bookingBadge, requireAuth: true, }, - ...(props.inviteShareEligible + ...(props.isAdmin ? [{ - key: 'invite', + key: 'teaching-schedule', type: 'item' as const, - title: '邀请好友', - path: '/pages/profile/invite', + title: '我的课表', + path: '/pages/profile/teaching-schedule', requireAuth: true, }] : []), + // 临时隐藏邀请好友入口,后续恢复时直接取消这段注释即可。 + // ...(props.inviteShareEligible + // ? [{ + // key: 'invite', + // type: 'item' as const, + // title: '邀请好友', + // path: '/pages/profile/invite', + // requireAuth: true, + // }] + // : []), { key: 'info', type: 'item', @@ -236,6 +246,29 @@ function handleTap(item: MenuItem) { } } + &--teaching-schedule { + background: rgba(93, 140, 138, 0.12); + &::before { + content: ''; + width: 24rpx; + height: 22rpx; + border: 2.5rpx solid #476d72; + border-radius: 6rpx; + box-sizing: border-box; + } + &::after { + content: ''; + position: absolute; + left: 50%; + top: 50%; + width: 12rpx; + height: 12rpx; + transform: translate(-30%, -18%) rotate(45deg); + border-top: 2.5rpx solid #476d72; + border-left: 2.5rpx solid #476d72; + } + } + &--invite { background: rgba(255, 122, 69, 0.12); &::before { diff --git a/packages/app/src/manifest.json b/packages/app/src/manifest.json index 68117eb..57fca40 100644 --- a/packages/app/src/manifest.json +++ b/packages/app/src/manifest.json @@ -1,12 +1,12 @@ { "name": "普拉提约课", - "appid": "", + "appid": "wx3e7a133d2305fa2c", "description": "普拉提工作室约课小程序", "versionName": "0.1.0", "versionCode": "100", "transformPx": false, "mp-weixin": { - "appid": "", + "appid": "wx3e7a133d2305fa2c", "setting": { "urlCheck": false, "es6": true, diff --git a/packages/app/src/pages.json b/packages/app/src/pages.json index 9af1ab6..bc1de1f 100644 --- a/packages/app/src/pages.json +++ b/packages/app/src/pages.json @@ -45,6 +45,12 @@ "navigationStyle": "custom" } }, + { + "path": "pages/profile/teaching-schedule", + "style": { + "navigationStyle": "custom" + } + }, { "path": "pages/profile/info", "style": { diff --git a/packages/app/src/pages/profile/teaching-schedule.vue b/packages/app/src/pages/profile/teaching-schedule.vue new file mode 100644 index 0000000..9fe7ac6 --- /dev/null +++ b/packages/app/src/pages/profile/teaching-schedule.vue @@ -0,0 +1,582 @@ + + + + + diff --git a/packages/app/src/stores/booking.ts b/packages/app/src/stores/booking.ts index 5d8368a..c017684 100644 --- a/packages/app/src/stores/booking.ts +++ b/packages/app/src/stores/booking.ts @@ -6,6 +6,7 @@ import type { BookingWithUser, BookingStatusHistory, CreateBookingDto, + TeachingScheduleSlot, } from '@mp-pilates/shared' import { get, post, put } from '../utils/request' @@ -21,8 +22,10 @@ export const useBookingStore = defineStore('booking', () => { const slots = ref([]) const myBookings = ref([]) const upcomingBookings = ref([]) + const teachingSchedule = ref([]) const loadingSlots = ref(false) const loadingBookings = ref(false) + const loadingTeachingSchedule = ref(false) async function fetchSlots(date: string) { loadingSlots.value = true @@ -70,6 +73,21 @@ export const useBookingStore = defineStore('booking', () => { } } + async function fetchTeachingSchedule(date: string) { + loadingTeachingSchedule.value = true + try { + const result = await get('/admin/teaching-schedule', { date }) + teachingSchedule.value = Array.isArray(result) ? result : [] + return teachingSchedule.value + } catch (err) { + console.error('Fetch teaching schedule failed:', err) + teachingSchedule.value = [] + throw err + } finally { + loadingTeachingSchedule.value = false + } + } + // ─── Admin methods ────────────────────────────────────────────────────── async function fetchAllAdminBookings( @@ -124,13 +142,16 @@ export const useBookingStore = defineStore('booking', () => { slots, myBookings, upcomingBookings, + teachingSchedule, loadingSlots, loadingBookings, + loadingTeachingSchedule, fetchSlots, createBooking, cancelBooking, fetchMyBookings, fetchUpcomingBookings, + fetchTeachingSchedule, fetchAllAdminBookings, confirmBooking, completeBooking, diff --git a/packages/app/src/stores/user.ts b/packages/app/src/stores/user.ts index f0859c4..7c6e43d 100644 --- a/packages/app/src/stores/user.ts +++ b/packages/app/src/stores/user.ts @@ -11,6 +11,10 @@ import { get, put } from '../utils/request' import { ROUTES } from '../utils/routes' import { cacheSubscriptionMessageTemplateConfig, resetSubscriptionMessageTemplateCache } from '../utils/wechat-subscription' +function syncSubscriptionTemplates(profile?: Pick | null) { + cacheSubscriptionMessageTemplateConfig(profile?.subscriptionMessageTemplates) +} + export const useUserStore = defineStore('user', () => { // State const user = ref(null) @@ -36,7 +40,7 @@ export const useUserStore = defineStore('user', () => { const result = await wxLogin() token.value = result.token user.value = result.user - cacheSubscriptionMessageTemplateConfig(result.user.subscriptionMessageTemplates) + syncSubscriptionTemplates(result.user) return { user: result.user, isNewUser: result.isNewUser } } catch (err) { console.error('Login failed:', err) @@ -62,7 +66,7 @@ export const useUserStore = defineStore('user', () => { if (!isLoggedIn()) return try { user.value = await get('/user/profile') - cacheSubscriptionMessageTemplateConfig(user.value.subscriptionMessageTemplates) + syncSubscriptionTemplates(user.value) return user.value } catch (err) { console.error('Fetch profile failed:', err) @@ -90,13 +94,13 @@ export const useUserStore = defineStore('user', () => { async function updateProfile(data: { nickname?: string; avatarUrl?: string }) { const updated = await put('/user/profile', data) user.value = updated - cacheSubscriptionMessageTemplateConfig(updated.subscriptionMessageTemplates) + syncSubscriptionTemplates(updated) return updated } function setProfile(profile: UserProfileResponse) { user.value = profile - cacheSubscriptionMessageTemplateConfig(profile.subscriptionMessageTemplates) + syncSubscriptionTemplates(profile) } function checkAuth() { diff --git a/packages/app/src/utils/wechat-subscription.ts b/packages/app/src/utils/wechat-subscription.ts index bd40056..b553950 100644 --- a/packages/app/src/utils/wechat-subscription.ts +++ b/packages/app/src/utils/wechat-subscription.ts @@ -103,10 +103,18 @@ async function fetchTemplateConfig(): Promise return config } -export function cacheSubscriptionMessageTemplateConfig(config: SubscriptionMessageTemplateConfig): SubscriptionMessageTemplateConfig { - const normalized: SubscriptionMessageTemplateConfig = { - templates: config.templates.filter((item) => item.templateId), +function normalizeTemplateConfig(config?: Partial | null): SubscriptionMessageTemplateConfig { + const templates = Array.isArray(config?.templates) ? config.templates : [] + + return { + templates: templates.filter((item): item is SubscriptionMessageTemplate => !!item?.templateId), } +} + +export function cacheSubscriptionMessageTemplateConfig( + config?: Partial | null, +): SubscriptionMessageTemplateConfig { + const normalized = normalizeTemplateConfig(config) cachedConfig = normalized uni.setStorageSync(TEMPLATE_CONFIG_STORAGE_KEY, normalized) return normalized diff --git a/packages/server/src/auth/__tests__/auth.service.spec.ts b/packages/server/src/auth/__tests__/auth.service.spec.ts index 9aa2364..6694211 100644 --- a/packages/server/src/auth/__tests__/auth.service.spec.ts +++ b/packages/server/src/auth/__tests__/auth.service.spec.ts @@ -1,7 +1,8 @@ import { Test, TestingModule } from '@nestjs/testing' import { JwtService } from '@nestjs/jwt' import { UnauthorizedException } from '@nestjs/common' -import { UserRole } from '@mp-pilates/shared' +import { MembershipStatus, UserRole } from '@mp-pilates/shared' +import { ConfigService } from '@nestjs/config' import { AuthService, RANDOM_FN_TOKEN } from '../auth.service' import { WechatService } from '../wechat.service' import { PrismaService } from '../../prisma/prisma.service' @@ -23,6 +24,7 @@ const mockUser = { nickname: TEST_NICKNAME, avatarUrl: null, role: UserRole.MEMBER, + adminBookingSubscriptionCount: 0, createdAt: new Date(), updatedAt: new Date(), } @@ -30,6 +32,9 @@ const mockUser = { // ─── Mocks ─────────────────────────────────────────────────────────────────── const mockPrismaService = { + membership: { + count: jest.fn(), + }, user: { findUnique: jest.fn(), findUniqueOrThrow: jest.fn(), @@ -51,6 +56,10 @@ const mockInviteService = { bindInviterToUser: jest.fn(), } +const mockConfigService = { + get: jest.fn(), +} + // ─── Tests ─────────────────────────────────────────────────────────────────── describe('AuthService', () => { @@ -64,6 +73,7 @@ describe('AuthService', () => { { provide: WechatService, useValue: mockWechatService }, { provide: JwtService, useValue: mockJwtService }, { provide: InviteService, useValue: mockInviteService }, + { provide: ConfigService, useValue: mockConfigService }, { provide: RANDOM_FN_TOKEN, useValue: () => 0 }, // deterministic nickname ], }).compile() @@ -72,6 +82,8 @@ describe('AuthService', () => { jest.clearAllMocks() mockJwtService.sign.mockReturnValue(JWT_TOKEN) + mockPrismaService.membership.count.mockResolvedValue(0) + mockConfigService.get.mockReturnValue('tmpl-booking-confirmed') }) // ── login ────────────────────────────────────────────────────────────────── @@ -99,7 +111,17 @@ describe('AuthService', () => { expect(mockPrismaService.user.create).toHaveBeenCalledWith({ data: { openid: OPENID, nickname: TEST_NICKNAME, adminBookingSubscriptionCount: 0 }, }) - expect(result.user).toEqual(mockUser) + expect(result.user).toEqual(expect.objectContaining({ + id: mockUser.id, + phone: mockUser.phone, + nickname: mockUser.nickname, + avatarUrl: mockUser.avatarUrl, + role: mockUser.role, + activeMembershipCount: 0, + inviteShareEligible: false, + adminBookingSubscriptionCount: 0, + })) + expect(result.user.subscriptionMessageTemplates.templates).toHaveLength(2) expect(result.isNewUser).toBe(true) expect(mockInviteService.bindInviterToUser).toHaveBeenCalledWith(USER_ID, undefined) }) @@ -139,7 +161,11 @@ describe('AuthService', () => { where: { openid: OPENID }, }) expect(mockPrismaService.user.create).not.toHaveBeenCalled() - expect(result.user).toEqual(mockUser) + expect(result.user).toEqual(expect.objectContaining({ + id: mockUser.id, + nickname: mockUser.nickname, + role: mockUser.role, + })) expect(result.isNewUser).toBe(false) }) @@ -160,11 +186,35 @@ describe('AuthService', () => { const result = await authService.login(loginCode) - expect(result).toEqual({ + expect(result).toEqual(expect.objectContaining({ token: JWT_TOKEN, - user: mockUser, isNewUser: false, + })) + expect(result.user).toEqual(expect.objectContaining({ + id: mockUser.id, + subscriptionMessageTemplates: { + templates: [ + expect.objectContaining({ scene: 'BOOKING_CREATED' }), + expect.objectContaining({ scene: 'ADMIN_BOOKING_CREATED' }), + ], + }, + })) + }) + + it('includes active membership count and invite eligibility in login response', async () => { + mockPrismaService.user.findUnique.mockResolvedValue(mockUser) + mockPrismaService.membership.count.mockResolvedValue(2) + + const result = await authService.login(loginCode) + + expect(mockPrismaService.membership.count).toHaveBeenCalledWith({ + where: { + userId: USER_ID, + status: MembershipStatus.ACTIVE, + }, }) + expect(result.user.activeMembershipCount).toBe(2) + expect(result.user.inviteShareEligible).toBe(true) }) }) diff --git a/packages/server/src/auth/auth.controller.ts b/packages/server/src/auth/auth.controller.ts index daa8cd7..8242393 100644 --- a/packages/server/src/auth/auth.controller.ts +++ b/packages/server/src/auth/auth.controller.ts @@ -1,12 +1,13 @@ import { - Controller, - Post, Body, - UseGuards, - Request, + Controller, HttpCode, HttpStatus, + Post, + Request, + UseGuards, } from '@nestjs/common' +import type { UserProfileResponse } from '@mp-pilates/shared' import { AuthService } from './auth.service' import { LoginDto } from './dto/login.dto' import { BindPhoneDto } from './dto/bind-phone.dto' @@ -24,7 +25,7 @@ export class AuthController { @Post('login') @HttpCode(HttpStatus.OK) - async login(@Body() loginDto: LoginDto): Promise<{ token: string; user: User; isNewUser: boolean }> { + async login(@Body() loginDto: LoginDto): Promise<{ token: string; user: UserProfileResponse; isNewUser: boolean }> { return this.authService.login( loginDto.code, loginDto.nickname, diff --git a/packages/server/src/auth/auth.module.ts b/packages/server/src/auth/auth.module.ts index 7ba2849..76b7da3 100644 --- a/packages/server/src/auth/auth.module.ts +++ b/packages/server/src/auth/auth.module.ts @@ -2,6 +2,7 @@ import { Module } from '@nestjs/common' import { PassportModule } from '@nestjs/passport' import { JwtModule } from '@nestjs/jwt' import { ConfigModule, ConfigService } from '@nestjs/config' +import { MembershipModule } from '../membership/membership.module' import { AuthService, RANDOM_FN_TOKEN } from './auth.service' import { AuthController } from './auth.controller' import { WechatService } from './wechat.service' @@ -14,6 +15,8 @@ import { InviteModule } from '../invite/invite.module' imports: [ PassportModule, InviteModule, + ConfigModule, + MembershipModule, JwtModule.registerAsync({ imports: [ConfigModule], inject: [ConfigService], diff --git a/packages/server/src/auth/auth.service.ts b/packages/server/src/auth/auth.service.ts index b6c4d77..3aa10c0 100644 --- a/packages/server/src/auth/auth.service.ts +++ b/packages/server/src/auth/auth.service.ts @@ -1,14 +1,22 @@ import { Inject, Injectable, UnauthorizedException } from '@nestjs/common' import { JwtService } from '@nestjs/jwt' import { User } from '@prisma/client' -import { UserRole } from '@mp-pilates/shared' +import { + MembershipStatus, + SubscriptionMessageScene, + type SubscriptionMessageTemplate, + type SubscriptionMessageTemplateConfig, + type UserProfileResponse, + UserRole, +} from '@mp-pilates/shared' +import { ConfigService } from '@nestjs/config' import { PrismaService } from '../prisma/prisma.service' import { WechatService } from './wechat.service' import { InviteService } from '../invite/invite.service' export interface LoginResult { token: string - user: User + user: UserProfileResponse isNewUser: boolean } @@ -57,9 +65,53 @@ export class AuthService { private readonly jwtService: JwtService, private readonly wechatService: WechatService, private readonly inviteService: InviteService, + private readonly configService: ConfigService, @Inject(RANDOM_FN_TOKEN) private readonly randomFn: () => number = Math.random, ) {} + private buildSubscriptionTemplateConfig(): SubscriptionMessageTemplateConfig { + const templates = [ + { + templateId: this.configService.get('WX_SUBSCRIBE_TEMPLATE_BOOKING_CONFIRMED', ''), + scene: SubscriptionMessageScene.BOOKING_CREATED, + description: '购卡或预约时请求一次订阅,用于后续预约确认通知推送', + usageTarget: 'consent' as const, + }, + { + templateId: this.configService.get('WX_SUBSCRIBE_TEMPLATE_BOOKING_CONFIRMED', ''), + scene: SubscriptionMessageScene.ADMIN_BOOKING_CREATED, + description: '管理员主动增加预约提醒次数,用于接收学员新预约通知', + usageTarget: 'counter' as const, + }, + ] satisfies SubscriptionMessageTemplate[] + + return { + templates: templates.filter((item) => item.templateId), + } + } + + private async mapLoginUser(user: User): Promise { + const activeMembershipCount = await this.prisma.membership.count({ + where: { + userId: user.id, + status: MembershipStatus.ACTIVE, + }, + }) + + return { + id: user.id, + phone: user.phone, + nickname: user.nickname, + avatarUrl: user.avatarUrl, + role: user.role as UserRole, + activeMembershipCount, + inviteShareEligible: activeMembershipCount > 0, + adminBookingSubscriptionCount: user.adminBookingSubscriptionCount, + subscriptionMessageTemplates: this.buildSubscriptionTemplateConfig(), + createdAt: user.createdAt.toISOString(), + } + } + async login( code: string, nickname?: string, @@ -96,7 +148,7 @@ export class AuthService { sessionKeyStore.set(updated.id, sessionKey) const payload: JwtPayload = { sub: updated.id, role: updated.role as UserRole } const token = this.jwtService.sign(payload) - return { token, user: updated, isNewUser: false } + return { token, user: await this.mapLoginUser(updated), isNewUser: false } } sessionKeyStore.set(user.id, sessionKey) @@ -108,7 +160,7 @@ export class AuthService { const payload: JwtPayload = { sub: user.id, role: user.role as UserRole } const token = this.jwtService.sign(payload) - return { token, user, isNewUser } + return { token, user: await this.mapLoginUser(user), isNewUser } } async bindPhone( diff --git a/packages/server/src/booking/__tests__/booking.service.spec.ts b/packages/server/src/booking/__tests__/booking.service.spec.ts index 8565c1e..e6a392c 100644 --- a/packages/server/src/booking/__tests__/booking.service.spec.ts +++ b/packages/server/src/booking/__tests__/booking.service.spec.ts @@ -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, + ) + }) + }) }) diff --git a/packages/server/src/booking/booking.controller.ts b/packages/server/src/booking/booking.controller.ts index d29c52c..909c7c7 100644 --- a/packages/server/src/booking/booking.controller.ts +++ b/packages/server/src/booking/booking.controller.ts @@ -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) diff --git a/packages/server/src/booking/booking.service.ts b/packages/server/src/booking/booking.service.ts index 49d7a8f..48d7567 100644 --- a/packages/server/src/booking/booking.service.ts +++ b/packages/server/src/booking/booking.service.ts @@ -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 { + 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 { diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index 24d9c15..6bc27a4 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -50,6 +50,8 @@ export type { Booking, BookingWithDetails, BookingWithUser, + TeachingScheduleStudent, + TeachingScheduleSlot, BookingStatusHistory, CreateBookingDto, Order, diff --git a/packages/shared/src/types/booking.ts b/packages/shared/src/types/booking.ts index de9baf4..b3935e4 100644 --- a/packages/shared/src/types/booking.ts +++ b/packages/shared/src/types/booking.ts @@ -37,6 +37,24 @@ export interface BookingWithUser extends BookingWithDetails { } } +export interface TeachingScheduleStudent { + readonly bookingId: string + readonly userId: string + readonly nickname: string + readonly phone: string | null + readonly status: BookingStatus +} + +export interface TeachingScheduleSlot { + readonly slotId: string + readonly date: string + readonly startTime: string + readonly endTime: string + readonly bookedCount: number + readonly capacity: number + readonly students: readonly TeachingScheduleStudent[] +} + export interface BookingStatusHistory { readonly id: string readonly bookingId: string diff --git a/packages/shared/src/types/index.ts b/packages/shared/src/types/index.ts index a82097d..095841e 100644 --- a/packages/shared/src/types/index.ts +++ b/packages/shared/src/types/index.ts @@ -11,7 +11,15 @@ export type { CardType, CreateCardTypeDto, UpdateCardTypeDto } from './card-type export type { Membership, MembershipWithCardType } from './membership' export type { WeekTemplate, WeekTemplateInput } from './week-template' export type { TimeSlot, TimeSlotWithBookingStatus, CreateManualSlotDto, ScheduleSlotPreview, PublishDaySlotItem, PublishDaySlotsDto } from './time-slot' -export type { Booking, BookingWithDetails, BookingWithUser, BookingStatusHistory, CreateBookingDto } from './booking' +export type { + Booking, + BookingWithDetails, + BookingWithUser, + TeachingScheduleStudent, + TeachingScheduleSlot, + BookingStatusHistory, + CreateBookingDto, +} from './booking' export type { Order, OrderWithDetails, CreateOrderDto, PaymentParams, CreateOrderResponse } from './order' export type { StudioConfig,