## 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>
7.9 KiB
7.9 KiB
Admin Scheduling - Quick Reference Guide
🎯 Quick Links to Key Files
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:
- Valid JWT token in
Authorization: Bearer <token>header - User role must be
UserRole.ADMIN - 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-templatedeletes 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: truein Prisma - Safe to re-run without creating duplicates
- Assumes
date + startTime + endTimeis 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
🔗 Related Components
- 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
skipDuplicatesto 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)