Files
mp-pilates/packages/app/src/stores/admin.ts
2026-04-15 23:25:09 +08:00

298 lines
9.5 KiB
TypeScript

import { defineStore } from 'pinia'
import { ref } from 'vue'
import { get, post, put, del } from '../utils/request'
import type {
CardType,
CreateCardTypeDto,
UpdateCardTypeDto,
StudioConfig,
UpdateStudioConfigDto,
OrderWithDetails,
TimeSlot,
CreateManualSlotDto,
PaginatedData,
ScheduleSlotPreview,
PublishDaySlotsDto,
FlashSaleAdminItem,
CreateFlashSaleDto,
UpdateFlashSaleDto,
CreateStudioUploadCredentialDto,
StudioUploadCredential,
} from '@mp-pilates/shared'
interface LegacyPaginatedData<T> {
readonly data: readonly T[]
readonly total: number
readonly page: number
readonly limit: number
}
function normalizePaginatedData<T>(
result: PaginatedData<T> | LegacyPaginatedData<T>,
): PaginatedData<T> {
if ('items' in result) {
return {
items: [...result.items],
total: result.total,
page: result.page,
limit: result.limit,
}
}
return {
items: [...result.data],
total: result.total,
page: result.page,
limit: result.limit,
}
}
export interface AdminStats {
todayBookings: number
totalOrders: number
totalBookings: number
}
export interface MemberSummary {
userId: string
openid: string
nickname: string
phone: string | null
avatarUrl: string | null
totalBookings: number
completedBookings: number
cancelledBookings: number
}
export interface UserMembership {
userId: string
membership: {
id: string
cardTypeId: string
remainingTimes: number | null
startDate: string
expireDate: string
status: string
cardType: {
id: string
name: string
type: string
totalTimes: number | null
durationDays: number
}
} | null
}
export const useAdminStore = defineStore('admin', () => {
// ── Card types ───────────────────────────────────────────────────
const cardTypes = ref<CardType[]>([])
async function fetchCardTypes(): Promise<CardType[]> {
const data = await get<CardType[]>('/admin/card-types')
cardTypes.value = [...data].sort((a, b) => a.sortOrder - b.sortOrder)
return cardTypes.value
}
async function createCardType(dto: CreateCardTypeDto): Promise<CardType> {
const data = await post<CardType>('/admin/card-types', dto as unknown as Record<string, unknown>)
await fetchCardTypes()
return data
}
async function updateCardType(id: string, dto: UpdateCardTypeDto): Promise<CardType> {
const data = await put<CardType>(`/admin/card-types/${id}`, dto as unknown as Record<string, unknown>)
await fetchCardTypes()
return data
}
async function deleteCardType(id: string): Promise<{ deleted: boolean; deactivated: boolean }> {
const result = await del<{ deleted: boolean; deactivated: boolean }>(`/admin/card-types/${id}`)
await fetchCardTypes()
return result
}
// ── Studio config ────────────────────────────────────────────────
const studioConfig = ref<StudioConfig | null>(null)
async function fetchStudioConfig(): Promise<StudioConfig> {
const data = await get<StudioConfig>('/studio/info')
studioConfig.value = data
return data
}
async function saveStudioConfig(dto: UpdateStudioConfigDto): Promise<StudioConfig> {
const data = await put<StudioConfig>('/admin/studio/info', dto as unknown as Record<string, unknown>)
studioConfig.value = data
return data
}
async function createStudioUploadCredential(
dto: CreateStudioUploadCredentialDto,
): Promise<StudioUploadCredential> {
return post<StudioUploadCredential>(
'/admin/studio/upload-credentials',
dto as unknown as Record<string, unknown>,
)
}
// ── Orders ───────────────────────────────────────────────────────
async function fetchAdminOrders(params: {
page?: number
limit?: number
status?: string
}): Promise<PaginatedData<OrderWithDetails>> {
const result = await get<PaginatedData<OrderWithDetails> | LegacyPaginatedData<OrderWithDetails>>(
'/admin/orders',
params,
)
return normalizePaginatedData(result)
}
// ── Bookings ─────────────────────────────────────────────────────
async function fetchAdminBookings(params?: {
page?: number
limit?: number
userId?: string
}): Promise<PaginatedData<any>> {
return get<PaginatedData<any>>('/admin/bookings', params)
}
// ── Members ──────────────────────────────────────────────────────
async function fetchMembers(params?: {
page?: number
limit?: number
search?: string
cardType?: string
}): Promise<PaginatedData<MemberSummary>> {
const cleanParams: Record<string, unknown> = {}
if (params?.page != null) cleanParams.page = params.page
if (params?.limit != null) cleanParams.limit = params.limit
if (params?.search) cleanParams.search = params.search
if (params?.cardType) cleanParams.cardType = params.cardType
return get<PaginatedData<MemberSummary>>('/admin/members', cleanParams)
}
async function getUserMembership(userId: string): Promise<UserMembership> {
return get<UserMembership>(`/admin/members/${userId}/membership`)
}
async function updateUserMembership(
userId: string,
dto: {
cardTypeId: string
remainingTimes?: number | null
startDate: string
expireDate: string
},
): Promise<any> {
return put<any>(`/admin/members/${userId}/membership`, dto)
}
async function deleteUserMembership(userId: string): Promise<void> {
return del<void>(`/admin/members/${userId}/membership`)
}
// ── Time slots ───────────────────────────────────────────────────
async function fetchSlotsByDate(date: string): Promise<TimeSlot[]> {
return get<TimeSlot[]>('/admin/time-slots', { date })
}
async function createManualSlot(dto: CreateManualSlotDto): Promise<TimeSlot> {
return post<TimeSlot>('/admin/time-slot/manual', dto as unknown as Record<string, unknown>)
}
async function closeSlot(id: string): Promise<TimeSlot> {
return put<TimeSlot>(`/admin/time-slot/${id}/close`, {})
}
async function generateSlots(startDate: string, endDate: string): Promise<{ count: number }> {
return post<{ count: number }>('/admin/generate-slots', { startDate, endDate })
}
// ── Schedule management ─────────────────────────────────────────
const schedulePreview = ref<ScheduleSlotPreview[]>([])
const scheduleLoading = ref(false)
async function fetchSchedulePreview(date: string): Promise<ScheduleSlotPreview[]> {
scheduleLoading.value = true
try {
const data = await get<ScheduleSlotPreview[]>('/admin/schedule/preview', { date })
schedulePreview.value = data
return data
} finally {
scheduleLoading.value = false
}
}
async function publishDaySlots(dto: PublishDaySlotsDto): Promise<void> {
await post('/admin/schedule/publish', dto as unknown as Record<string, unknown>)
await fetchSchedulePreview(dto.date)
}
// ── Dashboard stats ──────────────────────────────────────────────
async function fetchDashboardStats(): Promise<AdminStats> {
return get<AdminStats>('/admin/stats')
}
// ── Flash sales ─────────────────────────────────────────────────
async function fetchFlashSales(params?: {
page?: number
limit?: number
}): Promise<PaginatedData<FlashSaleAdminItem>> {
return get<PaginatedData<FlashSaleAdminItem>>('/admin/flash-sales', params as Record<string, unknown>)
}
async function createFlashSale(dto: CreateFlashSaleDto): Promise<FlashSaleAdminItem> {
return post<FlashSaleAdminItem>('/admin/flash-sales', dto as unknown as Record<string, unknown>)
}
async function updateFlashSale(id: string, dto: UpdateFlashSaleDto): Promise<FlashSaleAdminItem> {
return put<FlashSaleAdminItem>(`/admin/flash-sales/${id}`, dto as unknown as Record<string, unknown>)
}
async function deleteFlashSale(id: string): Promise<{ deleted: boolean }> {
return del<{ deleted: boolean }>(`/admin/flash-sales/${id}`)
}
return {
// State
cardTypes,
studioConfig,
schedulePreview,
scheduleLoading,
// Card types
fetchCardTypes,
createCardType,
updateCardType,
deleteCardType,
// Studio
fetchStudioConfig,
saveStudioConfig,
createStudioUploadCredential,
// Orders
fetchAdminOrders,
// Bookings
fetchAdminBookings,
// Members
fetchMembers,
getUserMembership,
updateUserMembership,
deleteUserMembership,
// Time slots
fetchSlotsByDate,
createManualSlot,
closeSlot,
generateSlots,
// Schedule
fetchSchedulePreview,
publishDaySlots,
// Stats
fetchDashboardStats,
// Flash sales
fetchFlashSales,
createFlashSale,
updateFlashSale,
deleteFlashSale,
}
})