## 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>
356 lines
9.0 KiB
Markdown
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
|
|
|