## Features ### Admin Schedule Page (`packages/app/src/pages/admin/schedule.vue`) - Interactive date-based slot editor for managing daily schedules - Real-time slot editing: start/end times, capacity adjustments - Slot deletion with conflict warnings when bookings exist - Add new slots with modal dialog - Live booking status display (booked count, people names) - Publish/Save changes with sync feedback - Revert unsaved changes with confirmation - Skeleton loading states and empty state handling - Responsive design with optimized mobile UX ### Backend Enhancements - **New DTO** (`PublishDaySlotsDto`): Structured slot publishing with validation - Date string validation - Slot array with existing slot IDs for updates - Time and capacity validation per slot - **Schedule Preview API** (`getSchedulePreview`): - Check for existing published slots - Fallback to active WeekTemplates for unpublished dates - Unified response format with isPublished flag - **Publish Slots API** (`publishDaySlots`): - Atomic transaction for consistency - Update existing slots with new times/capacity - Create new slots from template data - Delete unpublished slots or set to CLOSED if bookings exist - Prevent capacity reduction below existing bookings - Returns all published slots for feedback ### State Management - Enhanced admin store with schedule state - Support for pending/unsaved slot changes - Optimistic UI updates with server sync ### Documentation - Comprehensive scheduling system architecture docs - Quick reference for admin workflows - Flow diagrams and state transitions - Implementation guide for future maintenance ## Breaking Changes None ## Testing Recommendations - Create slots for future dates via schedule editor - Verify booking prevention for locked/full slots - Test capacity adjustments with existing bookings - Confirm template-based schedule generation - Verify transaction rollback on publish failures Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
24 KiB
WeChat Mini-Program Admin Scheduling/排课设置 - Complete Exploration Report
Date: 2026-04-05
Project: mp-pilates (WeChat mini-program for pilates studio bookings)
📋 Executive Summary
This is a Pilates studio booking management system with a comprehensive admin scheduling UI. The "排课设置" (Schedule Setup) feature allows admins to:
- Define recurring weekly class templates (时间模板)
- Manually add time slots for specific dates
- Close slots (临时调整 → 关闭时段)
- Batch generate slots from templates
The architecture uses:
- Frontend: Vue 3 + TypeScript (WeChat mini-program with Taro/UNI framework)
- Backend: NestJS + Prisma ORM
- State Management: Pinia (Vue state management)
- Database: Likely PostgreSQL/MySQL with Prisma
🗂️ File Structure
Frontend Admin Pages
packages/app/src/pages/admin/
├── index.vue # Admin dashboard with nav grid
├── week-template.vue # 📅 Scheduling/排课设置 - Main feature
├── slot-adjust.vue # 🔧 Temporary adjustments (3 tabs)
├── members.vue # 👥 Member management
├── orders.vue # 📋 Order management
├── card-types.vue # 💳 Card type management
└── studio.vue # 🏢 Studio settings
Stores
packages/app/src/stores/
└── admin.ts # Pinia store with all admin API calls
Backend API
packages/server/src/
├── time-slot/
│ ├── time-slot.controller.ts # Admin & member endpoints for slots
│ ├── time-slot.service.ts # Business logic for slots
│ ├── slot-generator.service.ts # Template-based slot generation
│ └── dto/
│ ├── week-template.dto.ts # Input validation
│ ├── create-manual-slot.dto.ts
│ └── query-slots.dto.ts
├── studio/
│ └── studio.controller.ts # Studio config (admin endpoints)
└── scheduler/ # Cron scheduler for auto-generation
Shared Types
packages/shared/src/types/
├── week-template.ts # WeekTemplate interface
├── time-slot.ts # TimeSlot interface
└── constants.ts # WEEKDAY_LABELS, SLOT_GENERATION_DAYS
🔑 Key Components
1. Admin Dashboard (index.vue)
File: packages/app/src/pages/admin/index.vue
Features:
- Display stats: today's bookings, total orders, total bookings
- Navigation grid to 6 admin modules:
- 📅 排课设置 →
/pages/admin/week-template - 🔧 临时调整 →
/pages/admin/slot-adjust - 👥 会员管理 →
/pages/admin/members - 📋 订单管理 →
/pages/admin/orders - 💳 卡种管理 →
/pages/admin/card-types - 🏢 工作室设置 →
/pages/admin/studio
- 📅 排课设置 →
Key Functions:
- navigate(path): Navigates to admin pages
- loadStats(): Fetches dashboard statistics via adminStore.fetchDashboardStats()
State:
const stats = ref<AdminStats>({ todayBookings: 0, totalOrders: 0, totalBookings: 0 })
const statsLoading = ref(false)
2. Week Template Management (week-template.vue) ✨ MAIN SCHEDULING UI
File: packages/app/src/pages/admin/week-template.vue
Route: /pages/admin/week-template
Purpose
Manage recurring weekly schedule templates. These are used to auto-generate time slots for future weeks.
Data Structure
interface WeekTemplate {
readonly id: string
readonly dayOfWeek: number // 1=Mon, 2=Tue, ..., 7=Sun (ISO format)
readonly startTime: string // HH:MM format
readonly endTime: string // HH:MM format
readonly capacity: number // Max bookings per slot
readonly isActive: boolean // Enable/disable template
readonly createdAt: string
readonly updatedAt: string
}
UI Sections
-
Toolbar
- Display template count: "共 N 条模板"
- "+ 新增时段" (Add new slot) button
-
Template List (Grouped by weekday)
- Days are sorted (Monday → Sunday)
- Each day shows count: "3 个时段"
- Each template row displays:
- Time range: "09:00 – 10:00"
- Capacity: "10 人"
- Actions:
- Toggle button: "启用"/"停用" (Enable/Disable)
- "编辑" (Edit)
- "删除" (Delete)
- Inactive templates are grayed out (opacity: 0.5)
-
Modal for Add/Edit
- Star date picker (1-7 for weekday)
- Start time picker
- End time picker
- Capacity input (number)
- Validation: time and capacity required
- Cancel/Confirm buttons
-
Save Bar (Fixed at bottom)
- Only shows when
isDirtyflag is true - "保存全部更改" button with loading state
- Only shows when
Key Functions
async fetchTemplates()
- Fetches all templates from backend
- Groups by dayOfWeek for display
- Clears isDirty flag
async handleSave()
- Maps local template state to API payload
- Calls adminStore.saveWeekTemplates(payload)
- Refreshes templates after save
- Shows success/error toast
function openAdd()
- Opens modal for creating new template
- Clears form
function openEdit(tpl)
- Opens modal in edit mode
- Populates form with existing values
function submitForm()
- Validates form (time and capacity required)
- Creates or updates template in memory
- Sets isDirty = true (triggers save bar)
function toggleTemplate(tpl)
- Toggles isActive flag
- Sets isDirty = true
function deleteTemplate(tpl)
- Shows confirmation modal
- Removes from array
- Sets isDirty = true
Local State Management
const templates = ref<LocalTemplate[]>([])
const loading = ref(false)
const saving = ref(false)
const isDirty = ref(false) // Tracks unsaved changes
const showModal = ref(false)
const editTarget = ref<LocalTemplate | null>(null)
const form = ref({
dayIdx: 0, // Selected day index (0-6)
startTime: '09:00',
endTime: '10:00',
capacityStr: '10',
})
const grouped = computed(() => {
// Groups templates by dayOfWeek for rendering
return Object.fromEntries(
Object.entries(map).sort(([a], [b]) => Number(a) - Number(b))
)
})
Example: Adding a Monday 9AM-10AM class
- User taps "+ 新增时段"
- Modal opens, form is reset to defaults
- User selects "周一" (Monday) from picker
- User confirms times and capacity
- New template object is pushed to
templatesarray isDirtyis set to true → save bar appears- User taps "保存全部更改"
- Store calls
PUT /admin/week-templatewith all templates - Backend deletes all old templates and creates new ones
- Frontend refetches and displays updated list
3. Slot Adjustment (slot-adjust.vue) - Temporary Slot Management
File: packages/app/src/pages/admin/slot-adjust.vue
Route: /pages/admin/slot-adjust
Purpose
Handle temporary/manual time slot operations:
- Add one-off slots for specific dates
- Close available slots
- Batch-generate slots from templates
UI Structure (3 Tabs)
Tab 0: "新增时段" (Add Manual Slot)
- Date picker (defaults to today)
- Start/End time pickers
- Capacity input
- Submit button: "新增时段"
- Endpoint:
POST /admin/time-slot/manual
interface CreateManualSlotDto {
date: string // YYYY-MM-DD
startTime: string // HH:MM
endTime: string // HH:MM
capacity?: number // Defaults to DEFAULT_SLOT_CAPACITY (1)
}
Tab 1: "关闭时段" (Close Slots)
- Date picker (defaults to today)
- Loads all slots for selected date
- Displays slot list with:
- Time range
- Status badge (OPEN/FULL/CLOSED)
- Booked count: "X/Y"
- Close button (if not already closed)
- Confirmation modal when closing
- Endpoint:
PUT /admin/time-slot/:id/close
Slot Status Colors:
OPEN → Green badge #27ae60
FULL → Orange badge #e67e22
CLOSED → Gray badge #999
Tab 2: "批量生成" (Batch Generate)
- Start date picker
- End date picker (defaults to +7 days)
- Hint: "将根据排课模板,自动生成所选日期范围内的时段"
- "Will auto-generate slots for selected date range based on schedule template"
- Submit button: "批量生成"
- Endpoint:
POST /admin/generate-slots
How it works:
- Frontend sends date range to backend
- Backend fetches all active WeekTemplates
- For each day in range, finds matching templates by weekday
- Creates TimeSlot records with
source: TEMPLATE - Uses
skipDuplicates: trueto avoid re-generating existing slots
// Backend example: If templates include:
// - Monday: 09:00-10:00, 18:00-19:00 (2 templates)
// - Wednesday: 10:00-11:00 (1 template)
//
// And user selects 2026-04-05 to 2026-04-11:
// - Mon 04-06: 2 slots generated
// - Wed 04-08: 1 slot generated
// Total: 3 slots (if these dates fall in range)
Key Functions
async submitAddSlot()
- POST /admin/time-slot/manual
- Shows success/error toast
async loadSlotsForClose()
- Fetches slots for closeDate via adminStore.fetchSlotsByDate(date)
- Sets slotsLoading flag
async closeSlot(slot)
- Confirmation modal
- PUT /admin/time-slot/:id/close
- Reloads slot list
async submitGenerate()
- POST /admin/generate-slots with date range
- Shows toast with count of generated slots
Local State
const activeTab = ref(0) // 0=Add, 1=Close, 2=Generate
const submitting = ref(false)
const slotsLoading = ref(false)
// Tab 0: Add form
const addForm = ref({
date: formatDate(new Date()),
startTime: '09:00',
endTime: '10:00',
capacityStr: '10',
})
// Tab 1: Close slots
const closeDate = ref(formatDate(new Date()))
const daySlots = ref<TimeSlot[]>([])
// Tab 2: Generate form
const genForm = ref({
startDate: formatDate(new Date()),
endDate: formatDate(new Date(Date.now() + 7 * 86400000)), // +7 days
})
4. Admin Store (Pinia)
File: packages/app/src/stores/admin.ts
API Methods Related to Scheduling
// ── Week templates ───────────────────────────────────────────────
async fetchWeekTemplates(): Promise<WeekTemplate[]>
// GET /admin/week-template
// Returns all templates for current studio
// Usage: Gets templates for display in week-template.vue
async saveWeekTemplates(templates: WeekTemplateInput[]): Promise<WeekTemplate[]>
// PUT /admin/week-template
// Body: { templates: [...] }
// Replaces ALL templates with new set (delete all, create new)
// Note: Backend uses transaction for atomicity
// ── Time slots ───────────────────────────────────────────────────
async fetchSlotsByDate(date: string): Promise<TimeSlot[]>
// GET /admin/time-slots?date=YYYY-MM-DD
// Returns all slots for a specific date
// Used in slot-adjust.vue Tab 1 (close slots)
async createManualSlot(dto: CreateManualSlotDto): Promise<TimeSlot>
// POST /admin/time-slot/manual
// Creates a one-off time slot
// Used in slot-adjust.vue Tab 0
async closeSlot(id: string): Promise<TimeSlot>
// PUT /admin/time-slot/:id/close
// Changes slot status from OPEN to CLOSED
// Used in slot-adjust.vue Tab 1
async generateSlots(startDate: string, endDate: string): Promise<{ count: number }>
// POST /admin/generate-slots
// Generates slots from active templates for date range
// Used in slot-adjust.vue Tab 2
// Returns: { count: number of newly created slots }
// ── Dashboard ────────────────────────────────────────────────────
async fetchDashboardStats(): Promise<AdminStats>
// GET /admin/stats
// Returns: { todayBookings, totalOrders, totalBookings }
// Used in index.vue
API Response Types
interface AdminStats {
todayBookings: number
totalOrders: number
totalBookings: number
}
interface WeekTemplate {
id: string
dayOfWeek: number // 1-7 (ISO weekday)
startTime: string // HH:MM
endTime: string // HH:MM
capacity: number
isActive: boolean
createdAt: string
updatedAt: string
}
interface TimeSlot {
id: string
date: string // YYYY-MM-DD
startTime: string
endTime: string
capacity: number
bookedCount: number
status: TimeSlotStatus // OPEN | FULL | CLOSED
source: TimeSlotSource // TEMPLATE | MANUAL
templateId: string | null
createdAt: string
updatedAt: string
}
🔌 Backend Architecture
Time Slot Controller
File: packages/server/src/time-slot/time-slot.controller.ts
Member Endpoints (Public)
GET /time-slot/available?date=YYYY-MM-DD
- Get available slots for a date
- Include booking status for current user
GET /time-slot/:id
- Get specific slot details
Admin Endpoints (Requires JWT + ADMIN role)
GET /admin/week-template
- Returns all WeekTemplates
- Ordered by: dayOfWeek ASC, startTime ASC
PUT /admin/week-template
- Request body: { templates: [...] }
- Replaces all templates (transaction-based)
- Validation: dayOfWeek 1-7, startTime/endTime strings
POST /admin/time-slot/manual
- Request body: { date, startTime, endTime, capacity? }
- Creates manual slot with source=MANUAL
- Capacity defaults to DEFAULT_SLOT_CAPACITY
PUT /admin/time-slot/:id/close
- Changes slot status to CLOSED
- Returns updated slot
POST /admin/generate-slots
- Generates slots from active templates
- Fetches templates where isActive=true
- Creates slots for next SLOT_GENERATION_DAYS (14 days by default)
- Uses skipDuplicates to make re-runs safe
- Returns: { count: number }
Time Slot Service
File: packages/server/src/time-slot/time-slot.service.ts
Key methods:
async getWeekTemplates(): Promise<WeekTemplate[]>
// Returns all templates sorted by day/time
async replaceWeekTemplates(items: Array<{...}>): Promise<any>
// Transaction-based replacement:
// 1. Delete all existing templates
// 2. Create new ones from items array
// 3. Return count of created
async createManualSlot(dto): Promise<TimeSlot>
// Creates slot with source=MANUAL, status=OPEN
async closeSlot(id: string): Promise<TimeSlot>
// Updates status to CLOSED
Slot Generator Service
File: packages/server/src/time-slot/slot-generator.service.ts
Key method:
async generateSlots(daysAhead: number = 14): Promise<number>
// 1. Fetches all WeekTemplates where isActive=true
// 2. For each of next N days:
// - Calculate ISO weekday (1=Mon, 7=Sun)
// - Find matching templates by dayOfWeek
// - Create TimeSlot records with source=TEMPLATE, templateId=id
// 3. Uses createMany with skipDuplicates=true
// 4. Returns count of newly created slots
//
// Key: Converts JS getDay() (0=Sun) to ISO weekday (1=Mon, 7=Sun)
async cleanupExpiredSlots(): Promise<number>
// Called by scheduler
// Closes all OPEN slots with date < today
async checkExpiredMemberships(): Promise<number>
// Called by scheduler
// Expires memberships past end date or with 0 sessions left
async completeBookings(): Promise<number>
// Called by scheduler
// Marks CONFIRMED bookings as COMPLETED if slot date passed
📊 Data Flow: "排课设置" User Journey
Scenario: Admin sets up class schedule for next week
-
Admin opens dashboard →
index.vue- Taps "排课设置" nav item
-
Admin navigates to Week Template page →
week-template.vueonMounted()→fetchTemplates()- Frontend:
GET /admin/week-template - Shows existing templates grouped by day
- Example display:
周一 09:00-10:00 10人 [启用] [编辑] [删除] 18:00-19:00 8人 [启用] [编辑] [删除] 周三 10:00-11:00 12人 [启用] [编辑] [删除]
-
Admin adds a new class → Click "+ 新增时段"
- Modal opens
- Select day, time, capacity
- Click "确认"
- Template added to local
templatesarray - Save bar appears at bottom
-
Admin edits existing template → Click "编辑"
- Modal opens with existing values
- Modify time/capacity
- Click "确认"
- Updated in local array
- Save bar shows if changed
-
Admin disables a template → Click "停用"
isActiveflipped to false- Template grayed out
- Save bar shows
-
Admin saves all changes → Click "保存全部更改"
- Loading state
- Frontend:
PUT /admin/week-templatewith all templates - Backend transaction:
BEGIN TRANSACTION DELETE FROM week_template INSERT INTO week_template (day_of_week, start_time, end_time, capacity, is_active) VALUES (...) COMMIT TRANSACTION - Success toast
- Frontend refetches templates
- Save bar disappears
-
Backend scheduler auto-generates slots
- Nightly cron (scheduler module)
- Calls
SlotGeneratorService.generateSlots(14) - Queries active WeekTemplates
- For each day in next 14 days:
- Checks what templates apply (by ISO weekday)
- Creates TimeSlot records
- Uses
skipDuplicatesto avoid duplicates on re-run
- Example output:
date: 2026-04-06 (Monday) 09:00-10:00 source=TEMPLATE templateId=abc123 18:00-19:00 source=TEMPLATE templateId=def456 date: 2026-04-08 (Wednesday) 10:00-11:00 source=TEMPLATE templateId=ghi789
-
Members can see and book the generated slots
- Frontend:
GET /time-slot/available?date=2026-04-06 - Members choose a slot and confirm booking
- Frontend:
📅 Constants & Utilities
Shared Constants
File: packages/shared/src/constants.ts
export const SLOT_GENERATION_DAYS = 14
// Number of days ahead to generate slots for
export const DEFAULT_SLOT_CAPACITY = 1
// Default capacity if not specified (for private lessons)
export const WEEKDAY_LABELS = ['', '周一', '周二', '周三', '周四', '周五', '周六', '周日']
// Index 0 is unused, 1-7 map to weekdays
// Used in dropdowns and display
export const DEFAULT_CANCEL_HOURS_LIMIT = 2
// Hours before slot to allow free cancellation
export const TIME_PERIODS = {
MORNING: { label: '上午', start: '06:00', end: '12:00' },
AFTERNOON: { label: '下午', start: '12:00', end: '18:00' },
EVENING: { label: '晚上', start: '18:00', end: '22:00' }
}
export const DATE_SELECTOR_DAYS = 7
Format Utilities
File: packages/app/src/utils/format.ts
formatDate(date: Date | string): string
// Converts to YYYY-MM-DD format
// Used for date pickers and API calls
getWeekdayLabel(date: Date | string): string
// Returns Chinese weekday (周一-周日)
isToday(date: Date | string): boolean
// Checks if date is today
getDateRange(days: number): Array<{ date, weekday, isToday }>
// Generates future N days' dates
Request Utility
File: packages/app/src/utils/request.ts
function request<T>(options: RequestOptions): Promise<T>
// Makes HTTP request with JWT auth
// Auto-refreshes token on 401
function get<T>(url: string, data?: Record<string, unknown>): Promise<T>
function post<T>(url: string, data?: Record<string, unknown>): Promise<T>
function put<T>(url: string, data?: Record<string, unknown>): Promise<T>
function del<T>(url: string, data?: Record<string, unknown>): Promise<T>
// Base URL logic:
// - Production: https://focus.richarjiang.com/api
// - Development: http://localhost:3000/api
🔐 Permission Model
Role: UserRole.ADMIN
Protected Endpoints
All /admin/* endpoints require:
- Valid JWT token
- Header:
Authorization: Bearer <token> - User role must be
ADMIN
Protected by:
@UseGuards(JwtAuthGuard, RolesGuard)@Roles(UserRole.ADMIN)
Auth Flow
- Admin logs in via auth module
- JWT token returned, stored in
uni.setStorageSync('token') - All requests include token in Authorization header
- If 401 response: clear token, show login prompt
- If 4xx/5xx: show error toast
🐛 Current Implementation Notes
Implemented Features ✅
- Week template CRUD (Create, Read, Update via replace)
- Manual slot creation
- Close individual slots
- Batch slot generation from templates
- UI for all three slot adjustment tabs
- Local state change tracking (isDirty)
- Modal form for adding/editing templates
- Grouping templates by weekday
- Status badges for slots (OPEN/FULL/CLOSED)
Missing/Stub Features ⚠️
fetchDashboardStats()API endpoint appears to be stubbedindex.vuecalls it but endpoint not found in backend- May need to implement in studio or payment controller
- No client-side validation errors displayed on API failures
- No confirmation before overwriting all templates
- No undo/restore from past template versions
Edge Cases to Watch 🔍
-
Timezone handling: All dates are treated as UTC midnight
- Slot generation uses
setUTCHours(0,0,0,0) - Frontend format displays as YYYY-MM-DD (local string)
- Slot generation uses
-
Duplicate slot prevention:
- Backend uses
skipDuplicates: truein createMany - Assumes date + startTime + endTime forms unique key
- Backend uses
-
Template replacement is atomic:
- All templates deleted, all new ones created in transaction
- If one row fails, entire operation rolls back
-
ISO weekday vs JS getDay():
- Shared code uses ISO: 1=Mon, 7=Sun
- Frontend picker displays Chinese labels
- Backend slot-generator converts JS getDay() to ISO
📱 UI Design Patterns
Colors & Styling
- Primary:
#1a1a2e(dark navy) - Accent:
#c9a87c(gold) - Success:
#27ae60(green) - Warning:
#e67e22(orange) - Danger:
#c0392b(red) - Background:
#f5f3f0(light beige)
Component Patterns
- Skeleton loaders: Shimmer animation for loading states
- Save bar: Fixed bottom bar shows only when changes exist
- Toggle buttons: Color indicates state (on=green, off=orange)
- Modals: Bottom-sheet style with backdrop
- Pickers: WeChat native pickers for date/time
- Badges: Color-coded status indicators
🚀 Deployment & Configuration
Frontend
- WeChat mini-program environment
- Base URL logic in
packages/app/src/utils/request.ts:// Production https://focus.richarjiang.com/api // Development http://localhost:3000/api
Backend
- NestJS server on port 3000
- Prisma ORM with database
- JWT authentication
- Role-based access control (RBAC)
📚 Related Files Summary
| File | Purpose | Type |
|---|---|---|
admin/index.vue |
Admin dashboard | Component |
admin/week-template.vue |
Schedule templates | Component ⭐ |
admin/slot-adjust.vue |
Manual slot ops | Component |
stores/admin.ts |
Admin API calls | Store |
time-slot.service.ts |
Slot business logic | Service |
slot-generator.service.ts |
Template-based generation | Service |
time-slot.controller.ts |
API endpoints | Controller |
week-template.ts |
Type definitions | Type |
constants.ts |
Shared constants | Config |
format.ts |
Date/time utilities | Utility |
🎯 Key Takeaways
- "排课设置" is the master schedule template management page
- Templates are ISO-weekday based (1=Monday, 7=Sunday)
- Slot generation is automated via backend scheduler, triggered by:
- Nightly cron job
- Or manual POST to
/admin/generate-slotsendpoint
- Save pattern: Local changes tracked, one "save all" API call with full template array
- Timezone: All operations use UTC midnight as boundaries
- Atomicity: Backend uses Prisma transactions for template replacement
- Permissions: All admin endpoints protected by JWT + ADMIN role guard