## 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>
11 KiB
Booking Page Documentation
📚 Overview
This folder contains comprehensive documentation for the WeChat Mini-Program booking system in the mp-pilates project (Uni-app + Vue 3).
📄 Documentation Files
-
BOOKING_PAGE_ANALYSIS.md ⭐ START HERE
- Complete file-by-file breakdown of all components
- Data flow diagrams
- API contract documentation
- Color scheme and styling details
- Potential issues and problems
-
COMPONENT_HIERARCHY.md
- Visual component tree structure
- State management flow (Pinia stores)
- API sequence diagrams
- State machine for slot cards
- Data transformations
-
QUICK_REFERENCE.md
- Code snippets for quick lookup
- Debugging tips and console commands
- Common issues and solutions
- Debugging checklist
- API examples
🎯 Quick Navigation
I want to understand...
...the overall flow → Read: BOOKING_PAGE_ANALYSIS.md → "Complete Data Flow Diagram" section
...how the UI is structured → Read: COMPONENT_HIERARCHY.md → "Component Tree" + "UI Layout Breakdown"
...where specific code is → Read: QUICK_REFERENCE.md → "Finding Specific Things"
...how to debug an issue → Read: QUICK_REFERENCE.md → "Common Issues & Solutions"
...the API contracts → Read: QUICK_REFERENCE.md → "API Contract Summary"
...the store state → Read: COMPONENT_HIERARCHY.md → "State Management Flow"
🏗️ Project Structure
packages/app/src/
├── pages/
│ └── booking/
│ └── index.vue # Main booking page (311 lines)
├── components/
│ ├── DateSelector.vue # Date picker (50 lines)
│ ├── TimePeriodFilter.vue # Time period filter (50 lines)
│ ├── SlotCard.vue # Individual slot card (230 lines)
│ └── BookingConfirmPopup.vue # Booking confirmation modal (430 lines)
├── stores/
│ ├── booking.ts # Booking state (72 lines)
│ └── user.ts # User/membership state (110 lines)
└── utils/
├── request.ts # API request utilities (80 lines)
└── format.ts # Date formatting utilities (50 lines)
packages/shared/src/
├── types/
│ ├── time-slot.ts # TimeSlot types
│ ├── api.ts # API response types
│ └── booking.ts # Booking types
├── constants.ts # TIME_PERIODS, etc
└── enums.ts # Enums (TimeSlotStatus, etc)
🔄 Data Flow at a Glance
Page Load
↓
[Check login + load memberships]
↓
Store: fetchSlots(today)
↓
API: GET /time-slot/available?date=TODAY
↓
State: bookingStore.slots = [TimeSlotWithBookingStatus[], ...]
↓
Computed: filteredSlots (optionally filtered by period)
↓
Render: SlotCard components
↓
User interaction:
- Tap date → loadSlots(newDate)
- Filter period → filteredSlots re-computed
- Book slot → onBookTap() → popup
- Confirm → createBooking() → refresh slots
- Cancel → cancelBooking() → refresh slots
🎭 Key Components
1. pages/booking/index.vue
Role: Main page that orchestrates everything State: selectedDate, selectedPeriod, showConfirmPopup, pendingSlot Stores: bookingStore, userStore Key computed: scrollHeight, filteredSlots
2. components/SlotCard.vue
Role: Displays individual time slot Props: slot (TimeSlotWithBookingStatus) Emits: book, cancel States: 4 button states based on status + isBookedByMe
3. components/DateSelector.vue
Role: Horizontal date picker Props: modelValue (YYYY-MM-DD) Data: dateRange (7 days from today) Display: Shows weekday, day number, month
4. components/TimePeriodFilter.vue
Role: Horizontal tab filter Props: modelValue (MORNING|AFTERNOON|EVENING|null) Constants: TIME_PERIODS from shared
5. components/BookingConfirmPopup.vue
Role: Modal for confirming booking Props: visible, slot, memberships State: selectedMembershipId (auto-selected on show) Logic: Auto-select first membership when popup opens
6. stores/booking.ts
Actions:
- fetchSlots(date) → GET /time-slot/available?date=
- createBooking(dto) → POST /booking
- cancelBooking(bookingId) → PUT /booking/:id/cancel
- fetchMyBookings(status?) → GET /booking/my
- fetchUpcomingBookings() → GET /booking/my/upcoming
7. stores/user.ts
Computed:
- loggedIn: !!token.value
- hasValidMembership: activeMemberships.length > 0
- activeMemberships: memberships filtered by ACTIVE status
📊 State Types
TimeSlotWithBookingStatus
{
id: string // UUID
date: "2026-04-05" // YYYY-MM-DD
startTime: "09:00" // HH:MM
endTime: "10:00" // HH:MM
capacity: 1 // Max slots
bookedCount: 0 // Currently booked
status: "OPEN" | "FULL" | "CLOSED"
source: "MANUAL" | "TEMPLATE"
templateId: null
isBookedByMe: boolean // User has booked this
myBookingId: string | null // Booking ID (for cancel)
}
MembershipWithCardType
{
id: string
cardType: { name: string, ... }
status: "ACTIVE" | "EXPIRED" | "USED_UP"
remainingTimes: number | null
expireDate: "2026-12-31"
}
🎨 Visual States
Slot Card Button States
| Condition | Button | Color | Action |
|---|---|---|---|
| OPEN, not booked | "可预约" | Tan (#c9a87c) | Show popup |
| OPEN, booked by me | "已预约" + "取消" link | Tan + Red | Show cancel confirm |
| FULL | "已约满" | Gray (#f0f0f0) | Disabled |
| CLOSED | "已关闭" | Gray (#f0f0f0) | Disabled |
Capacity Badge Colors
| Condition | Background | Text | Meaning |
|---|---|---|---|
| <80% booked | #f0faf3 | #4caf50 | Green - Plenty of spots |
| ≥80% booked | #fff8ed | #f59e0b | Orange - Almost full |
| FULL | #fef0f0 | #ef4444 | Red - No spots |
| CLOSED | #f5f5f5 | #999 | Gray - Unavailable |
🔐 Authentication
- Token stored in localStorage
- Automatically included in request headers
- 401 response → Clear token + show "please login" toast
- onBookTap checks loggedIn → shows login modal if needed
- onBookTap checks hasValidMembership → shows purchase modal if needed
📡 API Endpoints
GET /time-slot/available?date=YYYY-MM-DD
Query: date (required, YYYY-MM-DD format)
Returns: TimeSlotWithBookingStatus[]
Auth: Bearer token required
POST /booking
Body: { timeSlotId, membershipId }
Returns: BookingWithDetails
Auth: Bearer token required
PUT /booking/:bookingId/cancel
Path: bookingId
Returns: BookingWithDetails (with status: CANCELLED)
Auth: Bearer token required
GET /membership/my
Returns: MembershipWithCardType[]
Auth: Bearer token required
⚠️ Known Issues
1. GET Request Body Issue
- File:
utils/request.ts,get()function - Problem: Data passed as body instead of query params
- Impact: Might not work on all platforms
2. Error Handling
- File:
stores/booking.ts,fetchSlots() - Problem: Network error → empty array instead of error message
- Impact: Users can't tell if error or truly no slots
3. Loading State
- File:
pages/booking/index.vue - Problem: Skeleton only appears on initial load
- Impact: Date changes appear instant (confusing on slow network)
4. Date Math
- File:
utils/format.ts,getDateRange() - Problem: Uses ms arithmetic (86400000ms per day)
- Impact: Doesn't account for DST transitions
🧪 Testing Checklist
Happy Path
- Load page → today's slots display
- Tap date → slots change for that date
- Filter by period → slots filtered correctly
- Tap "可预约" → popup shows
- Confirm booking → slot shows "已预约"
- Tap "取消" → booking cancelled, slot resets
- Pull to refresh → slots reload
Edge Cases
- No slots for date → empty state appears
- Not logged in → login modal on book tap
- No valid membership → purchase modal on book tap
- Network error → ??? (currently shows empty)
- Slot becomes FULL → button updates to disabled
- Multiple memberships → can select different card
📝 File Sizes
| File | Lines | Purpose |
|---|---|---|
| pages/booking/index.vue | 311 | Main page orchestration |
| components/BookingConfirmPopup.vue | 430 | Booking modal |
| components/SlotCard.vue | 230 | Slot display |
| stores/booking.ts | 72 | Booking state |
| utils/request.ts | 80 | API client |
| components/DateSelector.vue | 50 | Date picker |
| components/TimePeriodFilter.vue | 50 | Period filter |
| utils/format.ts | 50 | Date utilities |
🎓 Learning Path
Level 1: Overview
- Read this file
- Look at BOOKING_PAGE_ANALYSIS.md → "Complete Data Flow Diagram"
Level 2: Components
- Read COMPONENT_HIERARCHY.md → "Component Tree"
- Read BOOKING_PAGE_ANALYSIS.md → "File-by-File Analysis"
Level 3: Implementation
- Read QUICK_REFERENCE.md → "Where Slots Come From"
- Read actual source files in order:
- stores/booking.ts
- pages/booking/index.vue
- components/SlotCard.vue
- components/BookingConfirmPopup.vue
Level 4: Debugging
- Read QUICK_REFERENCE.md → "Debugging Tips"
- Read QUICK_REFERENCE.md → "Common Issues & Solutions"
Level 5: Deep Dive
- Read COMPONENT_HIERARCHY.md → "State Management Flow"
- Read COMPONENT_HIERARCHY.md → "API Calls Sequence"
- Study utils/request.ts for request handling
🔗 Related Documentation
- Backend:
/packages/server/src/time-slot/ - Shared types:
/packages/shared/src/types/ - Auth:
/packages/app/src/utils/auth.ts - User store:
/packages/app/src/stores/user.ts
📞 Quick Answers
Q: Why doesn't the page load? A: Check 1) Is API returning data? 2) Is token valid? 3) Check console for errors
Q: Why doesn't filtering work? A: Check 1) Is selectedPeriod.value being set? 2) Is slot.startTime correct format?
Q: Why doesn't the booking button work? A: Check 1) Is slot.status === OPEN? 2) Is isBookedByMe === false? 3) Is user logged in?
Q: How do I add error handling? A: See QUICK_REFERENCE.md → "Issue 1: Slots not loading" → Solution
Q: How do I test the booking flow? A: See "Testing Checklist" section above
🚀 Common Tasks
Add loading indicator during date change
→ Use bookingStore.loadingSlots in template
Show error message for API failures
→ Add error state to bookingStore, show in template
Change colors/styling
→ Edit style blocks in .vue files (see color scheme in BOOKING_PAGE_ANALYSIS.md)
Modify time period ranges
→ Edit TIME_PERIODS in packages/shared/src/constants.ts
Change initial date or time range
→ Edit pages/booking/index.vue onMounted() or DATE_SELECTOR_DAYS constant
Add/remove date selector days
→ Edit DATE_SELECTOR_DAYS in packages/shared/src/constants.ts
Generated: 2026-04-05 Last Updated: BOOKING_PAGE_ANALYSIS.md