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

356 lines
9.0 KiB
Markdown

# 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
```typescript
// 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
```typescript
// 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
```typescript
// 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:**
```typescript
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:
```bash
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 templates**`PUT /admin/week-template`
2. **Manually trigger generation**`POST /admin/generate-slots`
3. **View available slots**`GET /time-slot/available?date=...`
4. **Create booking**`POST /booking`
5. **Cancel booking**`PUT /booking/:id/cancel`
For testing without scheduler:
```typescript
// 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