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>
This commit is contained in:
296
SCHEDULING_QUICK_REFERENCE.md
Normal file
296
SCHEDULING_QUICK_REFERENCE.md
Normal file
@@ -0,0 +1,296 @@
|
||||
# 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)
|
||||
```typescript
|
||||
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)
|
||||
```typescript
|
||||
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)
|
||||
```json
|
||||
[
|
||||
{
|
||||
"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)
|
||||
```json
|
||||
{
|
||||
"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
|
||||
```json
|
||||
{
|
||||
"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
|
||||
```typescript
|
||||
// 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
|
||||
```typescript
|
||||
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`:
|
||||
```typescript
|
||||
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)**:
|
||||
```typescript
|
||||
// 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 `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)
|
||||
|
||||
Reference in New Issue
Block a user