## 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>
9.0 KiB
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
- Avoid N+1 queries - Always include relations in findMany
- Batch operations - Use createMany/updateMany for large operations
- Transactions - Wrap multi-step operations to prevent race conditions
- Indexes - Queries filter by date and status (both indexed)
Development Workflow
- Setup templates →
PUT /admin/week-template - Manually trigger generation →
POST /admin/generate-slots - View available slots →
GET /time-slot/available?date=... - Create booking →
POST /booking - Cancel booking →
PUT /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