- fix(booking): 修复未认领画像阻断约课问题,支持约课事务内自动认领绑定 - fix(app): 修复 Pinia 状态解构导致响应式失效,补齐 PortraitRadar 组件实例传参 - fix(admin): 修复经营助手复测待办点击 404,优化体验课后线索回访文案 - fix(server): 线下评估日期间隔按中国时区校验,复测按期次单项核销,优化单次评估分享卡与限流清理 - test: 补齐 body-portrait-offline 与 booking 来源画像单元测试 Co-authored-by: Cursor <cursoragent@cursor.com>
188 lines
5.9 KiB
TypeScript
188 lines
5.9 KiB
TypeScript
import { defineStore } from 'pinia'
|
|
import { ref } from 'vue'
|
|
import type {
|
|
TimeSlotWithBookingStatus,
|
|
BookingWithDetails,
|
|
BookingWithUser,
|
|
BookingStatusHistory,
|
|
CreateBookingDto,
|
|
TeachingScheduleSlot,
|
|
} from '@mp-pilates/shared'
|
|
import { get, post, put } from '../utils/request'
|
|
import { useBodyPortraitStore } from './body-portrait'
|
|
|
|
/** Server paginated responses use `data` field, not `items` from the shared type */
|
|
interface ServerPaginatedResult<T> {
|
|
readonly data: readonly T[]
|
|
readonly total: number
|
|
readonly page: number
|
|
readonly limit: number
|
|
}
|
|
|
|
export const useBookingStore = defineStore('booking', () => {
|
|
const slots = ref<readonly TimeSlotWithBookingStatus[]>([])
|
|
const myBookings = ref<readonly BookingWithDetails[]>([])
|
|
const upcomingBookings = ref<readonly BookingWithDetails[]>([])
|
|
const teachingSchedule = ref<readonly TeachingScheduleSlot[]>([])
|
|
const loadingSlots = ref(false)
|
|
const loadingBookings = ref(false)
|
|
const loadingTeachingSchedule = ref(false)
|
|
|
|
async function fetchSlots(date: string) {
|
|
loadingSlots.value = true
|
|
try {
|
|
slots.value = await get<TimeSlotWithBookingStatus[]>('/time-slot/available', { date })
|
|
} catch (err) {
|
|
console.error('Fetch slots failed:', err)
|
|
slots.value = []
|
|
} finally {
|
|
loadingSlots.value = false
|
|
}
|
|
}
|
|
|
|
async function createBooking(dto: CreateBookingDto) {
|
|
const portraitStore = useBodyPortraitStore()
|
|
const originAssessmentId = dto.originAssessmentId || (portraitStore.claimed ? portraitStore.assessmentId : undefined)
|
|
const result = await post<BookingWithDetails>('/booking', {
|
|
...dto,
|
|
...(originAssessmentId ? { originAssessmentId } : {}),
|
|
} as unknown as Record<string, unknown>)
|
|
return result
|
|
}
|
|
|
|
/**
|
|
* Replace a booking in `myBookings` by id. Preserves immutability: always
|
|
* returns a new array reference so Vue's computed/watchers pick up the change.
|
|
* If the booking isn't in the list (e.g. paginated out), leaves state untouched.
|
|
*/
|
|
function replaceBooking(updated: BookingWithDetails) {
|
|
const idx = myBookings.value.findIndex((b) => b.id === updated.id)
|
|
if (idx === -1) return
|
|
const next = myBookings.value.slice()
|
|
next[idx] = updated
|
|
myBookings.value = next
|
|
}
|
|
|
|
async function cancelBooking(bookingId: string) {
|
|
const result = await put<BookingWithDetails>(`/booking/${bookingId}/cancel`)
|
|
replaceBooking(result)
|
|
return result
|
|
}
|
|
|
|
async function fetchMyBookings(status?: string, opts: { silent?: boolean } = {}) {
|
|
if (!opts.silent) loadingBookings.value = true
|
|
try {
|
|
const params: Record<string, unknown> = status ? { status } : {}
|
|
const paginated = await get<ServerPaginatedResult<BookingWithDetails>>('/booking/my', params)
|
|
myBookings.value = Array.isArray(paginated.data) ? paginated.data : []
|
|
} catch (err) {
|
|
console.error('Fetch bookings failed:', err)
|
|
myBookings.value = []
|
|
} finally {
|
|
if (!opts.silent) loadingBookings.value = false
|
|
}
|
|
}
|
|
|
|
async function fetchUpcomingBookings() {
|
|
try {
|
|
const result = await get<BookingWithDetails[]>('/booking/my/upcoming')
|
|
upcomingBookings.value = Array.isArray(result) ? result : []
|
|
} catch (err) {
|
|
console.error('Fetch upcoming bookings failed:', err)
|
|
upcomingBookings.value = []
|
|
}
|
|
}
|
|
|
|
async function fetchTeachingSchedule(date: string) {
|
|
loadingTeachingSchedule.value = true
|
|
try {
|
|
const result = await get<TeachingScheduleSlot[]>('/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(
|
|
page = 1,
|
|
limit = 20,
|
|
status?: string,
|
|
): Promise<ServerPaginatedResult<BookingWithUser>> {
|
|
const params: Record<string, unknown> = { page, limit }
|
|
if (status) params.status = status
|
|
|
|
const paginated = await get<ServerPaginatedResult<BookingWithUser>>('/admin/bookings', params)
|
|
return paginated
|
|
}
|
|
|
|
async function confirmBooking(bookingId: string, remark?: string) {
|
|
const result = await put<BookingWithDetails>(`/booking/${bookingId}/confirm`, {
|
|
remark,
|
|
})
|
|
replaceBooking(result)
|
|
return result
|
|
}
|
|
|
|
async function completeBooking(bookingId: string, remark?: string) {
|
|
const result = await put<BookingWithDetails>(`/booking/${bookingId}/complete`, {
|
|
remark,
|
|
})
|
|
replaceBooking(result)
|
|
return result
|
|
}
|
|
|
|
async function markNoShow(bookingId: string, remark?: string) {
|
|
const result = await put<BookingWithDetails>(`/booking/${bookingId}/noshow`, {
|
|
remark,
|
|
})
|
|
replaceBooking(result)
|
|
return result
|
|
}
|
|
|
|
async function fetchBookingHistory(bookingId: string): Promise<BookingStatusHistory[]> {
|
|
const result = await get<BookingStatusHistory[]>(`/booking/${bookingId}/history`)
|
|
return result
|
|
}
|
|
|
|
async function fetchBookingById(bookingId: string) {
|
|
const result = await get<BookingWithDetails | BookingWithUser>(`/booking/${bookingId}`)
|
|
return result
|
|
}
|
|
|
|
async function fetchSlotById(slotId: string) {
|
|
const result = await get<TimeSlotWithBookingStatus>(`/time-slot/${slotId}`)
|
|
return result
|
|
}
|
|
|
|
return {
|
|
slots,
|
|
myBookings,
|
|
upcomingBookings,
|
|
teachingSchedule,
|
|
loadingSlots,
|
|
loadingBookings,
|
|
loadingTeachingSchedule,
|
|
fetchSlots,
|
|
createBooking,
|
|
cancelBooking,
|
|
fetchMyBookings,
|
|
fetchUpcomingBookings,
|
|
fetchTeachingSchedule,
|
|
fetchAllAdminBookings,
|
|
confirmBooking,
|
|
completeBooking,
|
|
markNoShow,
|
|
fetchBookingHistory,
|
|
fetchSlotById,
|
|
fetchBookingById,
|
|
replaceBooking,
|
|
}
|
|
})
|