Files
mp-pilates/docs/TIME_SLOT_QUICK_REFERENCE.md
richarjiang b6986ba30c feat(admin): implement full day-by-day schedule editor with live preview
## 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>
2026-04-05 12:18:49 +08:00

9.0 KiB

Time-Slot & Scheduling System - Quick Reference

File Locations

Component Path
Slot Generator packages/server/src/time-slot/slot-generator.service.ts
TimeSlot Service packages/server/src/time-slot/time-slot.service.ts
TimeSlot Controller packages/server/src/time-slot/time-slot.controller.ts
Scheduler packages/server/src/scheduler/scheduler.service.ts
Booking Service packages/server/src/booking/booking.service.ts
Booking Controller packages/server/src/booking/booking.controller.ts
Database Schema packages/server/prisma/schema.prisma
Shared Constants packages/shared/src/constants.ts
Shared Enums packages/shared/src/enums.ts

Key Concepts

WeekTemplate

Defines recurring class schedule by day of week (1=Monday, 7=Sunday) and time.

  • Used to auto-generate TimeSlots nightly
  • Can be enabled/disabled
  • Has capacity (default 1 for private lessons)

TimeSlot

Individual class instance on a specific date with a specific time.

  • Status: OPEN → FULL → CLOSED
  • Source: TEMPLATE (auto-generated) or MANUAL (admin-created)
  • Cannot have duplicates (unique constraint on date+startTime+endTime)

Booking

User's reservation for a specific TimeSlot.

  • Status: CONFIRMED → COMPLETED (or CANCELLED)
  • Links user + timeSlot + membership
  • Unique constraint: one booking per user per slot

Daily Scheduler Jobs

All times in UTC:

Time Job What It Does
02:00 handleSlotGeneration() Generate slots 14 days ahead from WeekTemplates
02:30 handleCleanupSlots() Mark past OPEN slots as CLOSED
03:00 handleCheckMemberships() Expire memberships by date or used-up sessions
22:00 handleCompleteBookings() Mark past CONFIRMED bookings as COMPLETED

Important Methods

SlotGeneratorService

// Generate N days of slots from WeekTemplates
generateSlots(daysAhead = 14): Promise<number>

// Close all past OPEN slots
cleanupExpiredSlots(): Promise<number>

// Expire memberships by date or session count
checkExpiredMemberships(): Promise<number>

// Mark past bookings as COMPLETED
completeBookings(): Promise<number>

TimeSlotService

// Get all slots for a date (with user's booking status if provided)
getAvailableSlots(date: string, userId?: string): Promise<TimeSlotWithBookingStatus[]>

// Manually create a one-off slot
createManualSlot(dto: CreateManualSlotDto): Promise<TimeSlot>

// Close a slot (prevent new bookings)
closeSlot(id: string): Promise<TimeSlot>

// Get/replace weekly templates
getWeekTemplates(): Promise<WeekTemplate[]>
replaceWeekTemplates(items: WeekTemplateItemDto[]): Promise<CreateBatchPayload>

BookingService

// Create a booking (validates slot/membership, updates counts)
createBooking(userId: string, dto: CreateBookingDto): Promise<BookingWithRelations>

// Cancel a booking (conditionally refunds membership)
cancelBooking(userId: string, bookingId: string): Promise<CancelBookingResult>

// Get user's bookings (paginated, filterable by status)
getMyBookings(userId: string, status?, page, limit): Promise<PaginatedResult>

// Get all CONFIRMED bookings for dates >= today
getUpcomingBookings(userId: string): Promise<BookingWithRelations[]>

API Endpoints

Member Endpoints

GET  /time-slot/available?date=2026-04-10
  → Returns slots for that date with user's booking status

GET  /time-slot/:id
  → Returns full slot details with all bookings

POST /booking
  Body: { "timeSlotId": "uuid", "membershipId": "uuid" }
  → Create a booking

PUT  /booking/:id/cancel
  → Cancel a booking (refund if within window)

GET  /booking/my?status=CONFIRMED&page=1&limit=10
  → Get user's bookings (paginated)

GET  /booking/my/upcoming
  → Get all upcoming CONFIRMED bookings

Admin Endpoints

GET  /admin/week-template
  → List all templates

PUT  /admin/week-template
  Body: { "templates": [ {...}, {...} ] }
  → Replace all templates (atomic)

POST /admin/time-slot/manual
  Body: { "date", "startTime", "endTime", "capacity" }
  → Create a one-off slot

PUT  /admin/time-slot/:id/close
  → Close a slot

POST /admin/generate-slots
  → Manually trigger slot generation

GET  /admin/bookings?page=1&limit=10&status=CONFIRMED
  → View all bookings (admin)

Status Values

TimeSlotStatus

  • OPEN: Accepts bookings (bookedCount < capacity)
  • FULL: At capacity (bookedCount >= capacity)
  • CLOSED: Past date or manually closed

BookingStatus

  • CONFIRMED: Active reservation
  • CANCELLED: User cancelled
  • COMPLETED: Slot time has passed
  • NO_SHOW: Marked manually

MembershipStatus

  • ACTIVE: Valid for booking
  • EXPIRED: End date passed
  • USED_UP: No remaining sessions (for TIMES/TRIAL)

CardTypeCategory

  • TIMES: N sessions (e.g., "5-pack")
  • DURATION: Valid for X days (e.g., "1-month")
  • TRIAL: Free trial sessions

Key Logic

Booking Creation Transaction

1. Validate TimeSlot exists and status = OPEN
2. Check user not already booked this slot
3. Validate Membership:
   - Belongs to user
   - Status = ACTIVE
   - Has capacity:
     * TIMES/TRIAL: remainingTimes > 0
     * DURATION: expireDate > NOW
4. CREATE Booking(CONFIRMED)
5. UPDATE TimeSlot:
   - bookedCount++
   - IF bookedCount >= capacity THEN status = FULL
6. UPDATE Membership (if time-based):
   - remainingTimes--
   - IF remainingTimes = 0 THEN status = USED_UP
7. Return with relations

Cancellation Refund Logic

cancelHoursLimit = 2  (configurable in StudioConfig)
slotStartTime = TimeSlot.date + TimeSlot.startTime
deadline = NOW + (cancelHoursLimit * hours)

IF slotStartTime >= deadline:
  Refund = TRUE
  Increment membership.remainingTimes
ELSE:
  Refund = FALSE
  No membership change

Weekday Mapping

ISO Standard (what WeekTemplate uses):

1 = Monday
2 = Tuesday
3 = Wednesday
4 = Thursday
5 = Friday
6 = Saturday
7 = Sunday

JavaScript getDay() (what Date does):

0 = Sunday
1 = Monday
2 = Tuesday
...
6 = Saturday

Conversion function:

function toIsoWeekday(jsDay: number): number {
  return jsDay === 0 ? 7 : jsDay
}

Database Constraints

TimeSlot

  • Unique: [date, startTime, endTime] - prevents duplicate slots
  • Index: date - for date range queries
  • Index: status - for filtering

Booking

  • Unique: [userId, timeSlotId] - one booking per user per slot
  • Index: userId - for user's bookings
  • Index: status - for status filtering

Configuration

Environment Variables

DATABASE_URL=mysql://...  (required)

From StudioConfig Table

cancelHoursLimit = 2  (hours before slot to allow free cancellation)

From Shared Constants

DEFAULT_SLOT_CAPACITY = 1
SLOT_GENERATION_DAYS = 14
DEFAULT_CANCEL_HOURS_LIMIT = 2

Common Errors

Error Cause Solution
TimeSlot not found Invalid slot ID Check slot exists
TimeSlot is not available Status ≠ OPEN Slot is FULL or CLOSED
You have already booked this slot Duplicate booking Check user's bookings
This membership does not belong to you Membership not user's Verify membership
Membership is not active Status ≠ ACTIVE Renew or purchase membership
No remaining times on this membership remainingTimes ≤ 0 Purchase more sessions
Membership has expired expireDate < NOW Renew membership
Cannot cancel booking with status Status ≠ CONFIRMED Can only cancel CONFIRMED bookings

Testing

Run tests with:

npm test -- slot-generator.service.spec.ts
npm test -- booking.service.spec.ts
npm test -- time-slot.service.spec.ts

Key test areas:

  • Slot generation from templates
  • Weekday mapping (JS vs ISO)
  • Booking creation with all validations
  • Cancellation with/without refund
  • Membership expiration

Performance Tips

  1. Avoid N+1 queries - Always include relations in findMany
  2. Batch operations - Use createMany/updateMany for large operations
  3. Transactions - Wrap multi-step operations to prevent race conditions
  4. Indexes - Queries filter by date and status (both indexed)

Development Workflow

  1. Setup templatesPUT /admin/week-template
  2. Manually trigger generationPOST /admin/generate-slots
  3. View available slotsGET /time-slot/available?date=...
  4. Create bookingPOST /booking
  5. Cancel bookingPUT /booking/:id/cancel

For testing without scheduler:

// Inject SlotGeneratorService and call directly
const count = await slotGenerator.generateSlots(7)

Architecture Highlights

Idempotent - Safe to re-run slot generation Transactional - Bookings are atomic Automated - 4 daily cron jobs maintain state Flexible - Supports multiple membership types Scalable - Batch operations, proper indexes Validating - DTO decorators + business logic checks