Files
mp-pilates/SCHEDULING_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

7.9 KiB

Admin Scheduling - Quick Reference Guide

Frontend Components

File Lines Purpose
packages/app/src/pages/admin/index.vue 1-177 Admin dashboard, 6 nav items
packages/app/src/pages/admin/week-template.vue 1-500 MAIN: Schedule template management
packages/app/src/pages/admin/slot-adjust.vue 1-428 3 tabs: add/close/generate slots
packages/app/src/stores/admin.ts 1-171 API calls (Pinia store)

Backend Services

File Purpose
packages/server/src/time-slot/time-slot.controller.ts API endpoints (/admin/*)
packages/server/src/time-slot/time-slot.service.ts Template & slot logic
packages/server/src/time-slot/slot-generator.service.ts Auto-generate slots from templates
packages/server/src/time-slot/dto/week-template.dto.ts Input validation

Shared Types & Constants

File Exports
packages/shared/src/types/week-template.ts WeekTemplate, WeekTemplateInput
packages/shared/src/types/time-slot.ts TimeSlot, CreateManualSlotDto
packages/shared/src/constants.ts WEEKDAY_LABELS, SLOT_GENERATION_DAYS, etc.

🔄 The Flow: In 30 Seconds

Admin edits templates
        ↓
isDirty = true → Save bar appears
        ↓
Admin taps "保存全部更改"
        ↓
PUT /admin/week-template (full array)
        ↓
Backend: DELETE all, CREATE new (atomic)
        ↓
Scheduler triggers (nightly or manual)
        ↓
POST /admin/generate-slots
        ↓
SlotGeneratorService fetches active templates
        ↓
For each day (next 14 days):
  Match templates by ISO weekday
  Create TimeSlot records (source=TEMPLATE)
        ↓
Members see slots and can book

📊 Core Entities

WeekTemplate (Database)

id: string                    // UUID
dayOfWeek: number            // 1=Mon, 2=Tue, ..., 7=Sun
startTime: string            // "09:00"
endTime: string              // "10:00"
capacity: number             // Max bookings
isActive: boolean            // Enabled/disabled
createdAt: string
updatedAt: string

TimeSlot (Database)

id: string
date: string                 // YYYY-MM-DD
startTime: string
endTime: string
capacity: number
bookedCount: number          // How many booked
status: "OPEN" | "FULL" | "CLOSED"
source: "TEMPLATE" | "MANUAL"
templateId: string | null    // Links to WeekTemplate
createdAt: string
updatedAt: string

🌐 API Endpoints

GET /admin/week-template

Returns all templates (ordered by dayOfWeek ASC, startTime ASC)

[
  {
    "id": "uuid1",
    "dayOfWeek": 1,
    "startTime": "09:00",
    "endTime": "10:00",
    "capacity": 10,
    "isActive": true,
    "createdAt": "2026-04-05T00:00:00Z",
    "updatedAt": "2026-04-05T00:00:00Z"
  }
]

PUT /admin/week-template

Replace all templates (atomic transaction)

{
  "templates": [
    { "dayOfWeek": 1, "startTime": "09:00", "endTime": "10:00", "capacity": 10, "isActive": true },
    { "dayOfWeek": 1, "startTime": "18:00", "endTime": "19:00", "capacity": 8, "isActive": true },
    { "dayOfWeek": 3, "startTime": "10:00", "endTime": "11:00", "capacity": 12, "isActive": false }
  ]
}

POST /admin/time-slot/manual

Create a one-off slot

{
  "date": "2026-04-15",
  "startTime": "14:00",
  "endTime": "15:00",
  "capacity": 10
}

PUT /admin/time-slot/:id/close

Close a slot (changes status to CLOSED)

POST /admin/generate-slots

Generate slots for next 14 days from active templates Response: { "count": 28 }


🎨 UI State Management

week-template.vue Local State

// Main data
templates: LocalTemplate[]           // All templates
grouped: Computed<Record<number, LocalTemplate[]>>  // By dayOfWeek

// UI states
loading: boolean                     // Initial fetch
saving: boolean                      // Save in progress
isDirty: boolean                     // Show save bar?
showModal: boolean                   // Show add/edit modal?
editTarget: LocalTemplate | null     // Editing which template?

// Modal form
form: {
  dayIdx: number                     // 0-6 (picker index)
  startTime: string                  // "09:00"
  endTime: string                    // "10:00"
  capacityStr: string                // User input as string
}

Key Computed

const grouped = computed(() => {
  // Groups templates by dayOfWeek for rendering
  // Sorts by day number ascending (1-7)
  // Returns: { 1: [...], 3: [...], 5: [...], ... }
})

🔐 Permissions & Auth

All /admin/* endpoints require:

  1. Valid JWT token in Authorization: Bearer <token> header
  2. User role must be UserRole.ADMIN
  3. Guards: @UseGuards(JwtAuthGuard, RolesGuard)

🧮 Important Constants

From packages/shared/src/constants.ts:

SLOT_GENERATION_DAYS = 14              // Generate 14 days ahead
DEFAULT_SLOT_CAPACITY = 1              // Private lesson default
DEFAULT_CANCEL_HOURS_LIMIT = 2         // Cancel up to 2 hours before
WEEKDAY_LABELS = [
  '',       // index 0 (unused)
  '周一',   // index 1 → dayOfWeek 1 (Monday)
  '周二',   // index 2 → dayOfWeek 2
  '周三',   // ... etc
  '周四',
  '周五',
  '周六',
  '周日'    // index 7 → dayOfWeek 7 (Sunday)
]

🐛 Common Gotchas

1. dayOfWeek vs JS getDay()

  • Frontend uses: ISO weekday (1=Mon, 7=Sun)
  • JS Date.getDay(): 0=Sun, 6=Sat
  • Backend converts: toIsoWeekday() in slot-generator.service.ts

2. Template Replace (Not Merge)

  • PUT /admin/week-template deletes all and creates new
  • NOT a merge/patch operation
  • Frontend must send complete array

3. isDirty Flag

  • Tracks any change locally (add/edit/delete/toggle)
  • Used to show/hide save bar
  • Cleared after successful save

4. Timezone

  • All dates stored as UTC midnight: setUTCHours(0,0,0,0)
  • Frontend displays as local YYYY-MM-DD strings
  • May cause off-by-one on day boundaries

5. Slot Generation

  • Uses skipDuplicates: true in Prisma
  • Safe to re-run without creating duplicates
  • Assumes date + startTime + endTime is unique

💡 Usage Example: Add a Monday 9AM Class

Frontend (week-template.vue):

// User clicks "+ 新增时段"
openAdd()
form.value = { dayIdx: 0, startTime: '09:00', endTime: '10:00', capacityStr: '10' }
showModal.value = true

// User confirms in modal
submitForm()
templates.value.push({
  _key: String(Date.now()),
  dayOfWeek: 1,        // dayOptions[0].value = Monday
  startTime: '09:00',
  endTime: '10:00',
  capacity: 10,
  isActive: true
})
isDirty.value = true   // ← Save bar appears

// User taps "保存全部更改"
handleSave()
payload = templates.value.map(t => ({...}))
await adminStore.saveWeekTemplates(payload)

// Backend creates transaction:
// DELETE FROM week_template
// INSERT INTO week_template (day_of_week, start_time, end_time, capacity, is_active)
// VALUES (1, '09:00', '10:00', 10, true)
// ... (all other templates)

// Frontend refetches and displays

  • Admin Members (pages/admin/members.vue): Shows member list
  • Admin Orders (pages/admin/orders.vue): Shows order history
  • Admin Card Types (pages/admin/card-types.vue): Manage membership cards
  • Admin Studio (pages/admin/studio.vue): Studio info settings

📈 Scalability Notes

Current Approach

  • Templates: Small dataset (typically < 50 records)
  • Slots: Generated in batches (14 days at a time)
  • Uses skipDuplicates to handle reruns safely

Bottlenecks

  • Template replacement deletes ALL and creates NEW (atomic but slow with 1000s)
  • Slot generation is serial (could be parallelized)
  • No pagination for templates (assumes all fit in memory)

Future Improvements

  • Batch template updates (don't replace all)
  • Pagination if templates > 100
  • Incremental slot generation (detect last generated date)